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

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

接下来再看看调用 wait 的方法

没啥花头,就是将当前线程加入到 _waitSet 这个双向链表中,然后再执行 ObjectMonitor::exit 方法来释放锁。


image.png


接下来再看看调用 notify 的方法

也没啥花头,就是从 _waitSet 头部拿节点,然后根据策略选择是放在 cxq 还是 EntryList 的头部或者尾部,并且进行唤醒。


image.png


至于 notifyAll 我就不分析了,一样的,无非就是做了个循环,全部唤醒。

至此 synchronized 的几个操作都齐活了,出去可以说自己深入研究过 synchronized 了。

现在再来看下这个图,应该心里很有数了。


image.png


为什么会有_cxq 和 _EntryList 两个列表来放线程?

因为会有多个线程会同时竞争锁,所以搞了个 _cxq 这个单向链表基于 CAS 来 hold 住这些并发,然后另外搞一个 _EntryList 这个双向链表,来在每次唤醒的时候搬迁一些线程节点,降低 _cxq 的尾部竞争。

引入自旋

synchronized 的原理大致应该都清晰了,我们也知道了底层会用到系统调用,会有较大的开销,那思考一下该如何优化?

从小标题就已经知道了,方案就是自旋,文章开头就已经说了,这里再提一提。

自旋其实就是空转 CPU,执行一些无意义的指令,目的就是不让出 CPU 等待锁的释放

正常情况下锁获取失败就应该阻塞入队,但是有时候可能刚一阻塞,别的线程就释放锁了,然后再唤醒刚刚阻塞的线程,这就没必要了。

所以在线程竞争不是很激烈的时候,稍微自旋一会儿,指不定不需要阻塞线程就能直接获取锁,这样就避免了不必要的开销,提高了锁的性能。

但是自旋的次数又是一个难点,在竞争很激烈的情况,自旋就是在浪费 CPU,因为结果肯定是自旋一会让之后阻塞。

所以 Java 引入的是自适应自旋,根据上次自旋次数,来动态调整自旋的次数,这就叫结合历史经验做事

注意这是重量级锁的步骤,别忘了文章开头说的~

至此,synchronized 重量级锁的原理应该就很清晰了吧? 小结一下

synchronized 底层是利用 monitor 对象,CAS 和 mutex 互斥锁来实现的,内部会有等待队列(cxq 和 EntryList)和条件等待队列(waitSet)来存放相应阻塞的线程。

未竞争到锁的线程存储到等待队列中,获得锁的线程调用 wait 后便存放在条件等待队列中,解锁和 notify 都会唤醒相应队列中的等待线程来争抢锁。

然后由于阻塞和唤醒依赖于底层的操作系统实现,系统调用存在用户态与内核态之间的切换,所以有较高的开销,因此称之为重量级锁。

所以又引入了自适应自旋机制,来提高锁的性能。


现在要引入轻量级锁了


我们再思考一下,是否有这样的场景:多个线程都是在不同的时间段来请求同一把锁,此时根本就用不需要阻塞线程,连 monitor 对象都不需要,所以就引入了轻量级锁这个概念,避免了系统调用,减少了开销。

在锁竞争不激烈的情况下,这种场景还是很常见的,可能是常态,所以轻量级锁的引入很有必要。

在介绍轻量级锁的原理之前,再看看之前 MarkWord 图。

image.png


轻量级锁操作的就是对象头的 MarkWord 。

如果判断当前处于无锁状态,会在当前线程栈的当前栈帧中划出一块叫 LockRecord 的区域,然后把锁对象的 MarkWord 拷贝一份到 LockRecord 中称之为 dhw(就是那个set_displaced_header 方法执行的)里。

然后通过 CAS 把锁对象头指向这个 LockRecord 。

轻量级锁的加锁过程:


image.png

如果当前是有锁状态,并且是当前线程持有的,则将 null 放到 dhw 中,这是重入锁的逻辑。


image.png

我们再看下轻量级锁解锁的逻辑:


image.png


逻辑还是很简单的,就是要把当前栈帧中 LockRecord 存储的 markword (dhw)通过 CAS 换回到对象头中。

如果获取到的 dhw 是 null 说明此时是重入的,所以直接返回即可,否则就是利用 CAS 换,如果 CAS 失败说明此时有竞争,那么就膨胀!


image.png


关于这个轻量级加锁我再多说几句。

每次加锁肯定是在一个方法调用中,而方法调用就是有栈帧入栈,如果是轻量级锁重入的话那么此时入栈的栈帧里面的 dhw 就是 null,否则就是锁对象的 markword。

这样在解锁的时候就能通过 dhw 的值来判断此时是否是重入的。


现在要引入偏向锁


我们再思考一下,是否有这样的场景:一开始一直只有一个线程持有这个锁,也不会有其他线程来竞争,此时频繁的 CAS 是没有必要的,CAS 也是有开销的。

所以 JVM 研究者们就搞了个偏向锁,就是偏向一个线程,那么这个线程就可以直接获得锁。

我们再看看这个图,偏向锁在第二行。


image.png


原理也不难,如果当前锁对象支持偏向锁,那么就会通过 CAS 操作:将当前线程的地址(也当做唯一ID)记录到 markword 中,并且将标记字段的最后三位设置为 101。

之后有线程请求这把锁,只需要判断 markword 最后三位是否为 101,是否指向的是当前线程的地址。

还有一个可能很多文章会漏的点,就是还需要判断 epoch 值是否和锁对象的中的 epoch 值相同。

如果都满足,那么说明当前线程持有该偏向锁,就可以直接返回。

这 epoch 干啥用的?


image.png


可以理解为是第几代偏向锁。

偏向锁在有竞争的时候是要执行撤销操作的,其实就是要升级成轻量级锁。

而当一类对象撤销的次数过多,比如有个 Yes 类的对象作为偏向锁,经常被撤销,次数到了一定阈值(XX:BiasedLockingBulkRebiasThreshold,默认为 20 )就会把当代的偏向锁废弃,把类的 epoch 加一。

所以当类对象和锁对象的 epoch 值不等的时候,当前线程可以将该锁重偏向至自己,因为前一代偏向锁已经废弃了。

不过为保证正在执行的持有锁的线程不能因为这个而丢失了锁,偏向锁撤销需要所有线程处于安全点,然后遍历所有线程的 Java 栈,找出该类已加锁的实例,并且将它们标记字段中的 epoch 值加 1。

当撤销次数超过另一个阈值(XX:BiasedLockingBulkRevokeThreshold,默认值为 40),则废弃此类的偏向功能,也就是说这个类都无法偏向了。

至此整个 Synchronized 的流程应该都比较清楚了。

我是反着来讲锁升级的过程的,因为事实上是先有的重量级锁,然后根据实际分析优化得到的偏向锁和轻量级锁。


image.png


包括期间的一些细节应该也较为清楚了,我觉得对于 Synchronized 了解到这份上差不多了。

我再搞了张 openjdk wiki 上的图,看看是不是很清晰了:


image.png


最后


之所以分析源码,是因为看了资料,但是很多细节不清晰,然后很难受,所以没办法只能硬着头皮上了。

对于我这个 c++ 基本上不会的人来说,这个确实有点难度....断断续续写了一个星期。

其实没打算写这么多的,就只是想写自旋那一部分的...搞着搞着就停不下来了。

还有,如果有什么错误,赶紧联系我

这文章代码有点多,不知道有多少人可以耐着性子看到这里...

我觉得看到这里的都是高手啊!能不能扣个 1 给我看看?


相关文章
|
8月前
https://www.shijunheng.com 现在还不通过怎么回事呢
备案还不通过原因,https://www.shijunheng.com
86 0
https://www.shijunheng.com 现在还不通过怎么回事呢
|
11月前
|
C语言
C真的不难学,不信就看下我关于循环的理解
C真的不难学,不信就看下我关于循环的理解
|
算法 安全 Java
Java多线程与并发框(完结篇)——再看不懂我找不到女朋友
Java多线程与并发框(完结篇)——再看不懂我找不到女朋友
48 0
Java多线程与并发框(完结篇)——再看不懂我找不到女朋友
多线程顺序运行的 4 种方法,面试随便问!
多线程顺序运行的 4 种方法,面试随便问!
198 0
|
安全 Java
面试官:HashMap 中 modCount 变量有什么作用?大部分人都理解错了。。
面试官:HashMap 中 modCount 变量有什么作用?大部分人都理解错了。。
158 0
面试官:HashMap 中 modCount 变量有什么作用?大部分人都理解错了。。
|
存储 负载均衡 算法
HASH碰撞问题一直没真正搞懂?这下不用慌了
散列函数(英语:Hash function)又称散列算法、哈希函数,是一种从任何一种数据中创建小的数字“指纹”的方法。散列函数把消息或数据压缩成摘要,使得数据量变小,将数据的格式固定下来。该函数将数据打乱混合,重新创建一个叫做散列值(hash values,hash codes,hash sums,或hashes)的指纹。散列值通常用一个短的随机字母和数字组成的字符串来代表。
HASH碰撞问题一直没真正搞懂?这下不用慌了
|
Java 编译器 Linux
关于 Synchronized 的一个点,网上99%的文章都错了(上)
关于 Synchronized 的一个点,网上99%的文章都错了(上)
关于 Synchronized 的一个点,网上99%的文章都错了(上)
|
Java C++
关于 Synchronized 的一个点,网上99%的文章都错了(中)
关于 Synchronized 的一个点,网上99%的文章都错了(中)
关于 Synchronized 的一个点,网上99%的文章都错了(中)
|
Java 编译器 索引
异常是怎么被处理的?这题的答案不在源码里面。 (上)
异常是怎么被处理的?这题的答案不在源码里面。 (上)
81 0
异常是怎么被处理的?这题的答案不在源码里面。 (上)
|
Java
异常是怎么被处理的?这题的答案不在源码里面。 (下)
异常是怎么被处理的?这题的答案不在源码里面。 (下)
81 0
异常是怎么被处理的?这题的答案不在源码里面。 (下)