而关于 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 指令,我习惯按照临界区来