死磕 java同步系列之synchronized解析
问题
(1)synchronized的特性?
(2)synchronized的实现原理?
(3)synchronized是否可重入?
(4)synchronized是否是公平锁?
(5)synchronized的优化?
(6)synchronized的五种使用方式?
简介
synchronized关键字是Java里面最基本的同步手段,它经过编译之后,会在同步块的前后分别生成 monitorenter 和 monitorexit 字节码指令,这两个字节码指令都需要一个引用类型的参数来指明要锁定和解锁的对象。
实现原理
在学习Java内存模型的时候,我们介绍过两个指令:lock 和 unlock。
lock,锁定,作用于主内存的变量,它把主内存中的变量标识为一条线程独占状态。
unlock,解锁,作用于主内存的变量,它把锁定的变量释放出来,释放出来的变量才可以被其它线程锁定。
但是这两个指令并没有直接提供给用户使用,而是提供了两个更高层次的指令 monitorenter 和 monitorexit 来隐式地使用 lock 和 unlock 指令。
而 synchronized 就是使用 monitorenter 和 monitorexit 这两个指令来实现的。
根据JVM规范的要求,在执行monitorenter指令的时候,首先要去尝试获取对象的锁,如果这个对象没有被锁定,或者当前线程已经拥有了这个对象的锁,就把锁的计数器加1,相应地,在执行monitorexit的时候会把计数器减1,当计数器减小为0时,锁就释放了。
我们还是来上一段代码,看看编译后的字节码长啥样来学习:
public class SynchronizedTest{ public static void sync(){ synchronized(SynchronizedTest.class){ synchronized(SynchronizedTest.class){ } } } public static void main(String[] args){ } }
我们这段代码很简单,只是简单地对SynchronizedTest.class对象加了两次synchronized,除此之外,啥也没干。
编译后的sync()方法的字节码指令如下,为了便于阅读,彤哥特意加上了注释:
// 加载常量池中的SynchronizedTest类对象到操作数栈中 0 ldc #2 <com/coolcoding/code/synchronize/SynchronizedTest> // 复制栈顶元素 2 dup// 存储一个引用到本地变量0中,后面的0表示第几个变量 3 astore_0 // 调用monitorenter,它的参数变量0,也就是上面的SynchronizedTest类对象 4 monitorenter // 再次加载常量池中的SynchronizedTest类对象到操作数栈中 5 ldc #2 <com/coolcoding/code/synchronize/SynchronizedTest> // 复制栈顶元素 7 dup // 存储一个引用到本地变量1中 8 astore_1 // 再次调用monitorenter,它的参数是变量1,也还是SynchronizedTest类对象 9 monitorenter // 从本地变量表中加载第1个变量 10 aload_1 // 调用monitorexit解锁,它的参数是上面加载的变量1 11 monitorexit // 跳到第20行 12 goto 20 (+8) 15 astore_2 16 aload_1 17 monitorexit 18 aload_2 19 athrow // 从本地变量表中加载第0个变量 20 aload_0 // 调用monitorexit解锁,它的参数是上面加载的变量0 21 monitorexit // 跳到第30行 22 goto 30 (+8) 25 astore_326 aload_0 27 monitorexit28 aload_329 athrow // 方法返回,结束 30 return
按照彤哥的注释读起来,字节码比较简单,我们的synchronized锁定的是SynchronizedTest类对象,可以看到它从常量池中加载了两次SynchronizedTest类对象,分别存储在本地变量0和本地变量1中,解锁的时候正好是相反的顺序,先解锁变量1,再解锁变量0,实际上变量0和变量1指向的是同一个对象,所以synchronized是可重入的。
至于,被加锁的对象具体在对象头中是怎么存储的,彤哥这里就不细讲了,有兴趣的可以看看《Java并发编程的艺术》这本书。
原子性、可见性、有序性
前面讲解Java内存模型的时候我们说过内存模型主要就是用来解决缓存一致性的问题的,而缓存一致性主要包括原子性、可见性、有序性。
那么,synchronized关键字能否保证这三个特性呢?
还是回到Java内存模型上来,synchronized关键字底层是通过monitorenter和monitorexit实现的,而这两个指令又是通过lock和unlock来实现的。
而lock和unlock在Java内存模型中是必须满足下面四条规则的:
(1)一个变量同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一个线程执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才能被解锁。
(2)如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或assign操作初始化变量的值;
(3)如果一个变量没有被lock操作锁定,则不允许对其执行unlock操作,也不允许unlock一个其它线程锁定的变量;
(4)对一个变量执行unlock操作之前,必须先把此变量同步回主内存中,即执行store和write操作;
通过规则(1),我们知道对于lock和unlock之间的代码,同一时刻只允许一个线程访问,所以,synchronized是具有原子性的。
通过规则(1)(2)和(4),我们知道每次lock和unlock时都会从主内存加载变量或把变量刷新回主内存,而lock和unlock之间的变量(这里是指锁定的变量)是不会被其它线程修改的,所以,synchronized是具有可见性的。
通过规则(1)和(3),我们知道所有对变量的加锁都要排队进行,且其它线程不允许解锁当前线程锁定的对象,所以,synchronized是具有有序性的。
综上所述,synchronized是可以保证原子性、可见性和有序性的。
公平锁 VS 非公平锁
通过上面的学习,我们知道了synchronized的实现原理,并且它是可重入的,那么,它是否是公平锁呢?
直接上菜:
public class SynchronizedTest { public static void sync(String tips) { synchronized (SynchronizedTest.class) { System.out.println(tips); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } } } public static void main(String[] args) throws InterruptedException { new Thread(()->sync("线程1")).start(); Thread.sleep(100); new Thread(()->sync("线程2")).start(); Thread.sleep(100); new Thread(()->sync("线程3")).start(); Thread.sleep(100); new Thread(()->sync("线程4")).start(); } }
在这段程序中,我们起了四个线程,且分别间隔100ms启动,每个线程里面打印一句话后等待1000ms,如果synchronized是公平锁,那么打印的结果应该依次是 线程1、2、3、4。
但是,实际运行的结果几乎不会出现上面的样子,所以,synchronized是一个非公平锁。
锁优化
Java在不断进化,同样地,Java中像synchronized这种古老的东西也在不断进化,比如ConcurrentHashMap在jdk7的时候还是使用ReentrantLock加锁的,在jdk8的时候已经换成了原生的synchronized了,可见synchronized有原生的支持,它的进化空间还是很大的。
那么,synchronized有哪些进化中的状态呢?
我们这里稍做一些简单地介绍:
(1)偏向锁,是指一段同步代码一直被一个线程访问,那么这个线程会自动获取锁,降低获取锁的代价。
(2)轻量级锁,是指当锁是偏向锁时,被另一个线程所访问,偏向锁会升级为轻量级锁,这个线程会通过自旋的方式尝试获取锁,不会阻塞,提高性能。
(3)重量级锁,是指当锁是轻量级锁时,当自旋的线程自旋了一定的次数后,还没有获取到锁,就会进入阻塞状态,该锁升级为重量级锁,重量级锁会使其他线程阻塞,性能降低。
总结
(1)synchronized在编译时会在同步块前后生成monitorenter和monitorexit字节码指令;
(2)monitorenter和monitorexit字节码指令需要一个引用类型的参数,基本类型不可以哦;
(3)monitorenter和monitorexit字节码指令更底层是使用Java内存模型的lock和unlock指令;
(4)synchronized是可重入锁;
(5)synchronized是非公平锁;
(6)synchronized可以同时保证原子性、可见性、有序性;
(7)synchronized有三种状态:偏向锁、轻量级锁、重量级锁;
彩蛋——synchronized的五种使用方式
通过上面的分析,我们知道synchronized是需要一个引用类型的参数的,而这个引用类型的参数在Java中其实可以分成三大类:类对象、实例对象、普通引用,使用方式分别如下:
public class SynchronizedTest2 { public static final Object lock = new Object(); // 锁的是SynchronizedTest.class对象 public static synchronized void sync1() { } public static void sync2() { // 锁的是SynchronizedTest.class对象 synchronized (SynchronizedTest.class) { } } // 锁的是当前实例this public synchronized void sync3() { } public void sync4() { // 锁的是当前实例this synchronized (this){ } } public void sync5() { // 锁的是指定对象lock synchronized (lock) { } } }
总结
在方法上使用synchronized的时候要注意,会隐式传参,分为静态方法和非静态方法,静态方法上的隐式参数为当前类对象,非静态方法上的隐式参数为当前实例this。
另外,多个synchronized只有锁的是同一个对象,它们之间的代码才是同步的,这一点在使用synchronized的时候一定要注意。
这篇文章就到这里,希望可以给你带来帮助,也希望您可以多多关注猪先飞的更多内容!
相关文章
- 这篇文章主要介绍了如何利用java语言实现经典《复杂迷宫》游戏,文中采用了swing技术进行了界面化处理,感兴趣的小伙伴可以动手试一试...2022-02-01
java 运行报错has been compiled by a more recent version of the Java Runtime
java 运行报错has been compiled by a more recent version of the Java Runtime (class file version 54.0)...2021-04-01- 这篇文章主要介绍了在java中获取List集合中最大的日期时间操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2020-08-15
- 这篇文章主要介绍了教你怎么用Java获取国家法定节假日,文中有非常详细的代码示例,对正在学习java的小伙伴们有非常好的帮助,需要的朋友可以参考下...2021-04-23
- 这篇文章主要介绍了Java如何发起http请求的实现,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2021-03-31
- 说起C#和Java这两门语言(语法,数据类型 等),个人以为,大概有90%以上的相似,甚至可以认为几乎一样。但是在工作中,我也发现了一些细微的差别...2020-06-25
- 这篇文章主要介绍了解决Java处理HTTP请求超时的问题,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2021-03-29
- 这篇文章主要介绍了java 判断两个时间段是否重叠的案例,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2020-08-15
- 这篇文章主要介绍了超简洁java实现双色球若干注随机号码生成(实例代码),本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下...2021-04-02
- 这篇文章主要介绍了Java生成随机姓名、性别和年龄的实现示例,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧...2020-10-01
java 画pdf用itext调整表格宽度、自定义各个列宽的方法
这篇文章主要介绍了java 画pdf用itext调整表格宽度、自定义各个列宽的方法,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2021-01-31- 这篇文章主要介绍了java正则表达式判断前端参数修改表中另一个字段的值,需要的朋友可以参考下...2021-05-07
Java使用ScriptEngine动态执行代码(附Java几种动态执行代码比较)
这篇文章主要介绍了Java使用ScriptEngine动态执行代码,并且分享Java几种动态执行代码比较,需要的朋友可以参考下...2021-04-15- 这篇文章主要介绍了Java开发实现人机猜拳游戏,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下...2020-08-03
- 这篇文章主要介绍了c# 线程同步的相关资料,帮助大家更好的理解和学习c#,感兴趣的朋友可以了解下...2020-08-29
- 这篇文章主要介绍了Java List集合返回值去掉中括号('[ ]')的操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧...2020-08-29
- 1. MySQL数据库主从同步延迟原理。要说延时原理,得从mysql的数据库主从复制原理说起,mysql的主从复制都是单线程的操作,主库对所有DDL和DML产生binlog,binlog是顺序写,所以效率很高,slave的Slave_IO_Running线程到主库取日...2013-10-04
Java中lombok的@Builder注解的解析与简单使用详解
这篇文章主要介绍了Java中lombok的@Builder注解的解析与简单使用,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下...2021-01-06- 下面小编就为大家带来一篇java中String类型变量的赋值问题介绍。小编觉得挺不错的。现在分享给大家,给大家一个参考。...2016-03-28
Java 8 Stream 的终极技巧——Collectors 功能与操作方法详解
这篇文章主要介绍了Java 8 Stream Collectors 功能与操作方法,结合实例形式详细分析了Java 8 Stream Collectors 功能、操作方法及相关注意事项,需要的朋友可以参考下...2020-05-20