关于 Synchronized 的一个点,网上99%的文章都错了(上)

简介: 关于 Synchronized 的一个点,网上99%的文章都错了(上)

本来是在写面霸系列的,写着写着就写到了这一题:

Synchronized 原理知道不?

而关于 Synchronized 我去年还专门翻阅 JVM HotSpot 1.8 的源码来研究了一波,那时候我就发现有一个点,一个几乎网上所有文章包括《Java并发编程的艺术》也是这样说的一个点。


image.png

image.png


锁升级想必网上有太多文章说过了,这里提到当轻量级锁 CAS 失败,则当前线程会尝试使用自旋来获取锁

其实起初我也是这样认为的,毕竟都是这样说的,而且也很有道理。

因为重量级锁会阻塞线程,所以如果加锁的代码执行的非常快,那么稍微自旋一会儿其他线程就不需要锁了,就可以直接 CAS 成功了,因此不用阻塞了线程然后再唤醒。

但是我看了源码之后发现并不是这样的,这段代码在 synchronizer.cpp 中。


image.png


所以 CAS 失败了之后,并没有什么自旋操作,如果 CAS 成功就直接 return 了,如果失败会执行下面的锁膨胀方法。

我去锁膨胀的代码ObjectSynchronizer::inflate翻了翻,也没看到自旋操作。

所以从源码来看轻量级锁 CAS 失败并不会自旋而是直接膨胀成重量级锁

不过为了优化性能,自旋操作在 Synchronized 中确实却有。

那是在已经升级成重量级锁之后,线程如果没有争抢到锁,会进行一段自旋等待锁的释放。

咱们还是看源码说话,单单注释其实就已经说得很清楚了:


image.png


毕竟阻塞线程入队再唤醒开销还是有点大的。

我们再来看看 TrySpin 的操作,这里面有自适应自旋,其实从实际函数名就 TrySpin_VaryDuration 就可以反映出自旋是变化的。


image.png


至此,有关 Synchronized 自旋问题就完结了,重量级锁竞争失败会有自旋操作,轻量级锁没有这个动作(至少 1.8 源码是这样的),如果有人反驳你,请把这篇文章甩给他哈哈。

不过都说到这儿了,索性我就继续讲讲 Synchronized 吧,毕竟这玩意出镜率还是挺高的。

这篇文章关于 Synchronized 的深度到哪个程度呢?

之后如有面试官问你看过啥源码?

看完这篇文章,你可以回答:我看过 JVM 的源码

当然源码有点多的,我把 Synchronized 相关的所有操作都过了一遍,还是有点难度的。

不过之前看过我的源码分析的读者就会知道,我都会画个流程图来整理的,所以即使代码看不懂,流程还是可以搞清楚的!

好,发车!


从重量级锁开始说起


Synchronized 在1.6 之前只是重量级锁。

因为会有线程的阻塞和唤醒,这个操作是借助操作系统的系统调用来实现的,常见的 Linux 下就是利用 pthread 的 mutex 来实现的。

我截图了调用线程阻塞的源码,可以看到确实是利用了 mutex。


image.png

而涉及到系统调用就会有上下文的切换,即用户态和内核态的切换,我们知道这种切换的开销还是挺大的。

所以称为重量级锁,也因为这样才会有上面提到的自适应自旋操作,因为不希望走到这一步呀!

我们来看看重量级锁的实现原理

Synchronized 关键字可以修饰代码块,实例方法和静态方法,本质上都是作用于对象上

代码块作用于括号里面的对象,实例方法是当前的实例对象即 this ,而静态方法就是当前的类。


image.png


这里有个概念叫临界区

我们知道,之所以会有竞争是因为有共享资源的存在,多个线程都想要得到那个共享资源,所以就划分了一个区域,操作共享资源资源的代码就在区域内。

可以理解为想要进入到这个区域就必须持有锁,不然就无法进入,这个区域叫临界区。

当用 Synchronized 修饰代码块时

此时编译得到的字节码会有 monitorenter 和 monitorexit 指令,我习惯按照临界区来理解,enter 就是要进入临界区了,exit 就是要退出临界区了,与之对应的就是获得锁和解锁。

实际上这两个指令还是和修饰代码块的那个对象相关的,也就是上文代码中的lockObject

每个对象都有一个 monitor 对象于之关联,执行 monitorenter 指令的线程就是试图去获取 monitor 的所有权,抢到了就是成功获取锁了。

这个 monitor 下文会详细分析,我们先看下生成的字节码是怎样的。

图片上方是 lockObject 方法编译得到的字节码,下面就是 lockObject 方法,这样对着看比较容易理解。


image.png


从截图来看,执行 System.out 之前执行了 monitorenter 执行,这里执行争锁动作,拿到锁即可进入临界区。

调用完之后有个 monitorexit 指令,表示释放锁,要出临界区了。

图中我还标了一个 monitorexit 指令时,因为有异常的情况也需要解锁,不然就死锁了。

从生成的字节码我们也可以得知,为什么 synchronized 不需要手动解锁?

是有人在替我们负重前行啊!编译器生成的字节码都帮咱们做好了,异常的情况也考虑到了

当用 synchronized 修饰方法时

修饰方法生成的字节码和修饰代码块的不太一样,但本质上是一样。

此时字节码中没有 monitorenter 和 monitorexit 指令,不过在当前方法的访问标记上做了手脚。

我这里用的是 idea 的插件来看字节码,所以展示的字面结果不太一样,不过 flag 标记是一样的:0x0021 ,是 ACC_PUBLIC 和 ACC_SYNCHRONIZED 的结合。


image.png


原理就是修饰方法的时候在 flag 上标记 ACC_SYNCHRONIZED,在运行时常量池中通过 ACC_SYNCHRONIZED 标志来区分,这样 JVM 就知道这个方法是被 synchronized 标记的,于是在进入方法的时候就会进行执行争锁的操作,一样只有拿到锁才能继续执行。

然后不论是正常退出还是异常退出,都会进行解锁的操作,所以本质还是一样的。

这里还有个隐式的锁对象就是我上面提到的,修饰实例方法就是 this,修饰类方法就是当前类(关于这点是有坑的,我写的这篇文章分析过)。

我还记得有个面试题,好像是面字节跳动时候问的,面试官问 synchronized  修饰方法和代码块的时候字节码层面有什么区别?

怎么说?不知不觉距离字节跳动又更近了呢。


image.png



相关文章
|
3月前
|
搜索推荐 大数据 数据处理
面试官:try-catch 到底写在循环里面好,还是外面好?大部分人都会答错!
面试官:try-catch 到底写在循环里面好,还是外面好?大部分人都会答错!
46 0
C真的不难学,不信就看下我关于循环的理解
C真的不难学,不信就看下我关于循环的理解
|
JavaScript
for-in循环一看就懂
for-in循环一看就懂
56 0
|
算法 安全 Java
Java多线程与并发框(完结篇)——再看不懂我找不到女朋友
Java多线程与并发框(完结篇)——再看不懂我找不到女朋友
64 0
Java多线程与并发框(完结篇)——再看不懂我找不到女朋友
|
存储 负载均衡 算法
HASH碰撞问题一直没真正搞懂?这下不用慌了
散列函数(英语:Hash function)又称散列算法、哈希函数,是一种从任何一种数据中创建小的数字“指纹”的方法。散列函数把消息或数据压缩成摘要,使得数据量变小,将数据的格式固定下来。该函数将数据打乱混合,重新创建一个叫做散列值(hash values,hash codes,hash sums,或hashes)的指纹。散列值通常用一个短的随机字母和数字组成的字符串来代表。
HASH碰撞问题一直没真正搞懂?这下不用慌了
|
Java C++
关于 Synchronized 的一个点,网上99%的文章都错了(中)
关于 Synchronized 的一个点,网上99%的文章都错了(中)
关于 Synchronized 的一个点,网上99%的文章都错了(中)
|
存储 安全 Java
关于 Synchronized 的一个点,网上99%的文章都错了(下)
关于 Synchronized 的一个点,网上99%的文章都错了(下)
关于 Synchronized 的一个点,网上99%的文章都错了(下)
|
Java 编译器 索引
异常是怎么被处理的?这题的答案不在源码里面。 (上)
异常是怎么被处理的?这题的答案不在源码里面。 (上)
94 0
异常是怎么被处理的?这题的答案不在源码里面。 (上)
|
Java
异常是怎么被处理的?这题的答案不在源码里面。 (下)
异常是怎么被处理的?这题的答案不在源码里面。 (下)
103 0
异常是怎么被处理的?这题的答案不在源码里面。 (下)
|
Java 编译器 索引
异常是怎么被处理的?这题的答案不在源码里面。 (中)
异常是怎么被处理的?这题的答案不在源码里面。 (中)
110 0
异常是怎么被处理的?这题的答案不在源码里面。 (中)