本来是在写面霸系列的,写着写着就写到了这一题:
Synchronized 原理知道不?
而关于 Synchronized 我去年还专门翻阅 JVM HotSpot 1.8 的源码来研究了一波,那时候我就发现有一个点,一个几乎网上所有文章包括《Java并发编程的艺术》也是这样说的一个点。
锁升级想必网上有太多文章说过了,这里提到当轻量级锁 CAS 失败,则当前线程会尝试使用自旋来获取锁。
其实起初我也是这样认为的,毕竟都是这样说的,而且也很有道理。
因为重量级锁会阻塞线程,所以如果加锁的代码执行的非常快,那么稍微自旋一会儿其他线程就不需要锁了,就可以直接 CAS 成功了,因此不用阻塞了线程然后再唤醒。
但是我看了源码之后发现并不是这样的,这段代码在 synchronizer.cpp 中。
所以 CAS 失败了之后,并没有什么自旋操作,如果 CAS 成功就直接 return 了,如果失败会执行下面的锁膨胀方法。
我去锁膨胀的代码ObjectSynchronizer::inflate
翻了翻,也没看到自旋操作。
所以从源码来看轻量级锁 CAS 失败并不会自旋而是直接膨胀成重量级锁。
不过为了优化性能,自旋操作在 Synchronized 中确实却有。
那是在已经升级成重量级锁之后,线程如果没有争抢到锁,会进行一段自旋等待锁的释放。
咱们还是看源码说话,单单注释其实就已经说得很清楚了:
毕竟阻塞线程入队再唤醒开销还是有点大的。
我们再来看看 TrySpin
的操作,这里面有自适应自旋,其实从实际函数名就 TrySpin_VaryDuration
就可以反映出自旋是变化的。
至此,有关 Synchronized 自旋问题就完结了,重量级锁竞争失败会有自旋操作,轻量级锁没有这个动作(至少 1.8 源码是这样的),如果有人反驳你,请把这篇文章甩给他哈哈。
不过都说到这儿了,索性我就继续讲讲 Synchronized 吧,毕竟这玩意出镜率还是挺高的。
这篇文章关于 Synchronized 的深度到哪个程度呢?
之后如有面试官问你看过啥源码?
看完这篇文章,你可以回答:我看过 JVM 的源码。
当然源码有点多的,我把 Synchronized 相关的所有操作都过了一遍,还是有点难度的。
不过之前看过我的源码分析的读者就会知道,我都会画个流程图来整理的,所以即使代码看不懂,流程还是可以搞清楚的!
好,发车!
从重量级锁开始说起
Synchronized 在1.6 之前只是重量级锁。
因为会有线程的阻塞和唤醒,这个操作是借助操作系统的系统调用来实现的,常见的 Linux 下就是利用 pthread 的 mutex 来实现的。
我截图了调用线程阻塞的源码,可以看到确实是利用了 mutex。
而涉及到系统调用就会有上下文的切换,即用户态和内核态的切换,我们知道这种切换的开销还是挺大的。
所以称为重量级锁,也因为这样才会有上面提到的自适应自旋操作,因为不希望走到这一步呀!
我们来看看重量级锁的实现原理
Synchronized 关键字可以修饰代码块,实例方法和静态方法,本质上都是作用于对象上。
代码块作用于括号里面的对象,实例方法是当前的实例对象即 this ,而静态方法就是当前的类。
这里有个概念叫临界区。
我们知道,之所以会有竞争是因为有共享资源的存在,多个线程都想要得到那个共享资源,所以就划分了一个区域,操作共享资源资源的代码就在区域内。
可以理解为想要进入到这个区域就必须持有锁,不然就无法进入,这个区域叫临界区。
当用 Synchronized 修饰代码块时
此时编译得到的字节码会有 monitorenter 和 monitorexit 指令,我习惯按照临界区来理解,enter 就是要进入临界区了,exit 就是要退出临界区了,与之对应的就是获得锁和解锁。
实际上这两个指令还是和修饰代码块的那个对象相关的,也就是上文代码中的lockObject
。
每个对象都有一个 monitor 对象于之关联,执行 monitorenter 指令的线程就是试图去获取 monitor 的所有权,抢到了就是成功获取锁了。
这个 monitor 下文会详细分析,我们先看下生成的字节码是怎样的。
图片上方是 lockObject 方法编译得到的字节码,下面就是 lockObject 方法,这样对着看比较容易理解。
从截图来看,执行 System.out 之前执行了 monitorenter 执行,这里执行争锁动作,拿到锁即可进入临界区。
调用完之后有个 monitorexit 指令,表示释放锁,要出临界区了。
图中我还标了一个 monitorexit 指令时,因为有异常的情况也需要解锁,不然就死锁了。
从生成的字节码我们也可以得知,为什么 synchronized 不需要手动解锁?
是有人在替我们负重前行啊!编译器生成的字节码都帮咱们做好了,异常的情况也考虑到了。
当用 synchronized 修饰方法时
修饰方法生成的字节码和修饰代码块的不太一样,但本质上是一样。
此时字节码中没有 monitorenter 和 monitorexit 指令,不过在当前方法的访问标记上做了手脚。
我这里用的是 idea 的插件来看字节码,所以展示的字面结果不太一样,不过 flag 标记是一样的:0x0021 ,是 ACC_PUBLIC 和 ACC_SYNCHRONIZED 的结合。
原理就是修饰方法的时候在 flag 上标记 ACC_SYNCHRONIZED,在运行时常量池中通过 ACC_SYNCHRONIZED 标志来区分,这样 JVM 就知道这个方法是被 synchronized 标记的,于是在进入方法的时候就会进行执行争锁的操作,一样只有拿到锁才能继续执行。
然后不论是正常退出还是异常退出,都会进行解锁的操作,所以本质还是一样的。
这里还有个隐式的锁对象就是我上面提到的,修饰实例方法就是 this,修饰类方法就是当前类(关于这点是有坑的,我写的这篇文章分析过)。
我还记得有个面试题,好像是面字节跳动时候问的,面试官问 synchronized 修饰方法和代码块的时候字节码层面有什么区别?。
怎么说?不知不觉距离字节跳动又更近了呢。