详解Java并发编程之内置锁(synchronized)

 更新时间:2021年03月04日 08:45:50   投稿:mrr  
这篇文章主要介绍了Java并发编程之内置锁(synchronized)的相关知识,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下

简介

synchronized在JDK5.0的早期版本中是重量级锁,效率很低,但从JDK6.0开始,JDK在关键字synchronized上做了大量的优化,如偏向锁、轻量级锁等,使它的效率有了很大的提升。

synchronized的作用是实现线程间的同步,当多个线程都需要访问共享代码区域时,对共享代码区域进行加锁,使得每一次只能有一个线程访问共享代码区域,从而保证线程间的安全性。

因为没有显式的加锁和解锁过程,所以称之为隐式锁,也叫作内置锁、监视器锁。

如下实例,在没有使用synchronized的情况下,多个线程访问共享代码区域时,可能会出现与预想中不同的结果。

public class Apple implements Runnable {
 private int appleCount = 5;

 @Override
 public void run() {
  eatApple();
 }

 public void eatApple(){
  appleCount--;
  System.out.println(Thread.currentThread().getName() + "吃了一个苹果,还剩" + appleCount + "个苹果");
 }

 public static void main(String[] args) {
  Apple apple = new Apple();
  Thread t1 = new Thread(apple, "小强");
  Thread t2 = new Thread(apple, "小明");
  Thread t3 = new Thread(apple, "小花");
  Thread t4 = new Thread(apple, "小红");
  Thread t5 = new Thread(apple, "小黑");
  t1.start();
  t2.start();
  t3.start();
  t4.start();
  t5.start();
 }
}

可能会输出如下结果:

小强吃了一个苹果,还剩3个苹果
小黑吃了一个苹果,还剩3个苹果
小明吃了一个苹果,还剩2个苹果
小花吃了一个苹果,还剩1个苹果
小红吃了一个苹果,还剩0个苹果

输出结果异常的原因是eatApple方法里操作不是原子的,如当A线程完成appleCount的赋值,还没有输出,B线程获取到appleCount的最新值,并完成赋值操作,然后A和B同时输出。(A,B线程分别对应小黑、小强)

如果改下eatApple方法如下,还会不会有线程安全问题呢?

public void eatApple(){
	System.out.println(Thread.currentThread().getName() + "吃了一个苹果,还剩" + --appleCount + "个苹果");
}

还是会有的,因为--appleCount不是原子操作,--appleCount可以用另外一种写法表示:appleCount = appleCount - 1,还是有可能会出现以上的异常输出结果。

synchronized的使用

synchronized分为同步方法和同步代码块两种用法,当每个线程访问同步方法或同步代码块区域时,首先需要获得对象的锁,抢到锁的线程可以继续执行,抢不到锁的线程则阻塞,等待抢到锁的线程执行完成后释放锁。

1.同步代码块

锁的对象是object:

public class Apple implements Runnable {
 private int appleCount = 5;
 private Object object = new Object();

 @Override
 public void run() {
  eatApple();
 }

 public void eatApple(){
	//同步代码块,此时锁的对象是object
  synchronized (object) {
   appleCount--;
   System.out.println(Thread.currentThread().getName() + "吃了一个苹果,还剩" + appleCount + "个苹果");
  }
 }

  //...省略main方法
}

2.同步方法,修饰普通方法

锁的对象是当前类的实例对象:

public class Apple implements Runnable {
 private int appleCount = 5;

 @Override
 public void run() {
  eatApple();
 }

 public synchronized void eatApple() {
  appleCount--;
  System.out.println(Thread.currentThread().getName() + "吃了一个苹果,还剩" + appleCount + "个苹果");
 }

 //...省略main方法
}

等价于以下同步代码块的写法:

public void eatApple() {
	synchronized (this) {
		appleCount--;
		System.out.println(Thread.currentThread().getName() + "吃了一个苹果,还剩" + appleCount + "个苹果");
	}
}

3.同步方法,修饰静态方法

锁的对象是当前类的class对象:

public class Apple implements Runnable {
 private static int appleCount = 5;

 @Override
 public void run() {
  eatApple();
 }

 public synchronized static void eatApple() {
  appleCount--;
  System.out.println(Thread.currentThread().getName() + "吃了一个苹果,还剩" + appleCount + "个苹果");
 }

 //...省略main方法
}

等价于以下同步代码块的写法:

public static void eatApple() {
	synchronized (Apple.class) {
		appleCount--;
		System.out.println(Thread.currentThread().getName() + "吃了一个苹果,还剩" + appleCount + "个苹果");
	}
}

4.同步方法和同步代码块的区别

a.同步方法锁的对象是当前类的实例对象或者当前类的class对象,而同步代码块锁的对象可以是任意对象。

b.同步方法是使用synchronized修饰方法,而同步代码块是使用synchronized修饰共享代码区域。同步代码块相对于同步方法来说粒度更细,锁的区域更小,一般锁范围越小效率就越高。如下情况显然同步代码块更适用:

public static void eatApple() {
	//不需要同步的耗时操作1
	//...
	synchronized (Apple.class) {
		appleCount--;
		System.out.println(Thread.currentThread().getName() + "吃了一个苹果,还剩" + appleCount + "个苹果");
	}
	//不需要同步的耗时操作2
	//...
}

内置锁的可重入性

内置锁的可重入性是指当某个线程试图获取一个它已经持有的锁时,它总是可以获取成功。如下:

public static void eatApple() {
	synchronized (Apple.class) {
		synchronized (Apple.class) {
			synchronized (Apple.class) {
				appleCount--;
				System.out.println(Thread.currentThread().getName() + "吃了一个苹果,还剩" + appleCount + "个苹果");
			}
		}
	}
}

如果锁不是可重入的,那么假如某线程持有了该锁,然后又需要等待持有该锁的线程释放锁,这不就造成死锁了吗?

synchronized可以被继承吗?

synchronized不可以被继承,如果子类中重写后的方法需要实现同步,则需要手动添加synchronized关键字。

public class AppleParent {
 public synchronized void eatApple(){

 }
}

public class Apple extends AppleParent implements Runnable {
 private int appleCount = 5;

 @Override
 public void run() {
  eatApple();
 }

 @Override
 public void eatApple() {
  appleCount--;
  System.out.println(Thread.currentThread().getName() + "吃了一个苹果,还剩" + appleCount + "个苹果");
 }

 //...省略main方法
}

基于内置锁的等待和唤醒

基于内置锁的等待和唤醒是使用Object类中的wait()和notify()或notifyAll()来实现的。这些方法的调用前提是已经持有对应的锁,所以只能在同步方法或者同步代码块里调用。如果在没有获取到对应锁的情况下调用则会抛出IllegalMonitorStateException异常。下面介绍下相关的几个方法:

wait():使当前线程无限期地等待,直到另一个线程调用notify()或notifyAll()。

wait(long timeout):指定一个超时时间,超时时间过后线程将会被自动唤醒。线程也可以在超时时间之前被notify()或notifyAll()唤醒。注意,wait(0)等同于调用wait()。

wait(long timeout, int nanos):类似于wait(long timeout),主要区别是wait(long timeout, int nanos)提供了更高的精度。

notify():随机唤醒一个在相同锁对象上等待的线程。

notifyAll():唤醒所有在相同锁对象上等待的线程。

一个简单的等待唤醒实例:

public class Apple {
 //苹果数量
 private int appleCount = 0;

 /**
  * 买苹果
  */
 public synchronized void getApple() {
  try {
   while (appleCount != 0) {
    wait();
   }
  } catch (InterruptedException ex) {
   ex.printStackTrace();
  }

  System.out.println(Thread.currentThread().getName() + "买了5个苹果");
  appleCount = 5;
  notify();
 }

 /**
  * 吃苹果
  */
 public synchronized void eatApple() {
  try {
   while (appleCount == 0) {
    wait();
   }
  } catch (InterruptedException ex) {
   ex.printStackTrace();
  }

  System.out.println(Thread.currentThread().getName() + "吃了1个苹果");
  appleCount--;
  notify();
 }
}
/**
 * 生产者,买苹果
 */
public class Producer extends Thread{
 private Apple apple;

 public Producer(Apple apple, String name){
  super(name);
  this.apple = apple;
 }

 @Override
 public void run(){
  while (true)
  apple.getApple();
 }
}

/**
 * 消费者,吃苹果
 */
public class Consumer extends Thread{
 private Apple apple;

 public Consumer(Apple apple, String name){
  super(name);
  this.apple = apple;
 }

 @Override
 public void run(){
  while (true)
  apple.eatApple();
 }
}
public class Demo {
 public static void main(String[] args) {
  Apple apple = new Apple();
  Producer producer = new Producer(apple,"小明");
  Consumer consumer = new Consumer(apple, "小红");
  producer.start();
  consumer.start();
 }
}

输出结果:

小明买了5个苹果
小红吃了1个苹果
小红吃了1个苹果
小红吃了1个苹果
小红吃了1个苹果
小红吃了1个苹果
小明买了5个苹果
小红吃了1个苹果
    ......

到此这篇关于Java并发编程之内置锁(synchronized)的文章就介绍到这了,更多相关Java内置锁内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • 如何解决Could not transfer artifact org.springframework.boot问题

    如何解决Could not transfer artifact org.spri

    在Maven更新过程中遇到“Could not transfer artifact org.springframework.boot”错误通常是由于网络问题,解决方法是在Maven的设置中忽略HTTPS,添加特定语句后,可以正常下载依赖,但下载速度可能较慢,这是一种常见的解决方案,希望对遇到相同问题的人有所帮助
    2024-09-09
  • Java利用redis zset实现延时任务详解

    Java利用redis zset实现延时任务详解

    zset作为redis的有序集合数据结构存在,排序的依据就是score。本文就将利用zset score这个排序的这个特性,来实现延时任务,感兴趣的可以了解一下
    2022-08-08
  • SpringBoot @PropertySource与@ImportResource有什么区别

    SpringBoot @PropertySource与@ImportResource有什么区别

    这篇文章主要介绍了SpringBoot @PropertySource与@ImportResource有什么区别,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习吧
    2023-01-01
  • mybatis 解决将数值0识别成空字符串的问题

    mybatis 解决将数值0识别成空字符串的问题

    这篇文章主要介绍了mybatis 解决将数值0识别成空字符串的问题,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-06-06
  • Java内存模型final的内存语义

    Java内存模型final的内存语义

    这篇文章主要介绍了Java内存模型final的内存语义,上篇介绍volatile的内存语义,本文讲述的是final的内存语义,相比之下,final域的读和写更像是普通变量的访问。下面我们一起来看看文章学校内容吧,需要的朋友可以参考一下
    2021-11-11
  • Java基础之不简单的数组

    Java基础之不简单的数组

    数组(Array)是有序的元素序列。 若将有限个类型相同的变量的集合命名,那么这个名称为数组名。组成数组的各个变量称为数组的分量,也称为数组的元素,有时也称为下标变量
    2021-09-09
  • Java Chassis3应用视角的配置管理技术解密

    Java Chassis3应用视角的配置管理技术解密

    这篇文章主要为大家介绍了Java Chassis3应用视角的配置管理相关的机制和背后故事,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步,早日升职加薪
    2024-01-01
  • SpringBoot利用切面注解及反射实现事件监听功能

    SpringBoot利用切面注解及反射实现事件监听功能

    这篇文章主要介绍了springboot事件监听,通过利用切面、注解、反射实现,接下来将对这几种方式逐一说明,具有很好的参考价值,希望对大家有所帮助
    2022-07-07
  • 9种Java单例模式详解(推荐)

    9种Java单例模式详解(推荐)

    这篇文章主要介绍了9种Java单例模式详解,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2019-03-03
  • 详解@ConditionalOnMissingBean注解的作用

    详解@ConditionalOnMissingBean注解的作用

    这篇文章主要介绍了详解@ConditionalOnMissingBean注解的作用,@ConditionalOnMissingBean,它是修饰bean的一个注解,主要实现的是,当你的bean被注册之后,如果而注册相同类型的bean,就不会成功,它会保证你的bean只有一个,需要的朋友可以参考下
    2023-10-10

最新评论