JUC-06 详解Synchronized底层原理 part4

在上一篇的文章中,我们讲了Synchronized关键字底层实现部分的对象存储结构和对象头等内容,算上之前的文章,对于Synchronized关键字这块已经解析的比较全面了,今天我们就来收个尾,重点介绍一下锁膨胀的过程。

本篇文章,我们将围绕以下问题进行解析。

1.锁升级(膨胀)的过程你了解吗?

前置回顾:

首先,为了更好地回答上面的问题,我们先来回顾一下之前的内容。

MarkWord

其中包含了两个标记位。

一个标记位专门标记 偏向锁 or 无锁 状态。

一个标记位专门标记 轻量级锁 or 重量级锁 状态。

还有锁的四种不可逆的升级阶段。

无锁状态 -> 偏向锁状态 -> 轻量级锁状态 -> 重量级锁状态

必要概念:

这里有些重要的概念需要掌握,主要就是这些锁相关的概念点。

重量级锁、轻量级锁、偏向锁、自旋锁(锁竞争机制),在这一系列从重到轻的场景中,加锁的性能消耗是逐步递减的。

重量级锁-------> 性能消耗逐步减少 -----> 自旋锁

反之,我们的锁升级过程是性能逐步增加的,重量级锁的加减性能开销最大。

在我们的实际开发中,追求的方向永远是在实现需求的前提下,性能消耗最小。

重量级锁

  • 内置锁在Java中被抽象为监视器锁(monitor)。
  • JDK 1.6之前,监视器锁可以认为直接对应底层操作系统中的互斥量(mutex)。这种同步方式的成本非常高,包括系统调用引起的内核态与用户态切换、线程阻塞造成的线程切换等。因此,后来称这种锁为“重量级锁”。
  • 关键词:成本高(开销大)。

轻量级锁

  • 偏向锁膨胀之后的产物。
  • 作用&目的: 在没有多线程实际竞争的前提下,减少传统的重量级锁使用操作系统互斥量产生的性能消耗。
  • 当关闭偏向锁功能或者多个线程竞争偏向锁导致偏向锁升级为轻量级锁,则会尝试获取轻量级锁。
  • 适用场景:线程交替执行同步块的情况,如果存在同一时间访问同一锁的情况,必然就会导致轻量级锁膨胀为重量级锁。
  • 关键词:减少性能消耗,线程交替执行

偏向锁

  • 作用&目的:减少无竞争且只有一个线程使用锁的情况下,使用轻量级锁产生的性能消耗
  • 1.6中引入。参数-XX:-UseBiasedLocking来禁止偏向锁。
  • 偏向锁的释放采用了一种只有竞争才会释放锁(升级)的机制,线程是不会主动去释放偏向锁,需要等待其他线程来竞争。偏向锁的撤销需要等待全局安全点(这个时间点是上没有正在执行的代码)
  • 当一个线程访问同步块并获取锁时,会在对象头里存储锁偏向的线程ID(当前线程的栈帧中建立一个名为锁记录(Lock Record)的空间,用于存储锁对象目前的Mark Word的拷贝,官方称之为 Displaced Mark Word,以后该线程进入和退出同步块时不需要花费CAS操作来争夺锁资源,只需要检查是否为偏向锁、锁标识为以及ThreadID即可。
  • 关键词:减少性能消耗,无竞争且只有一个线程

自旋锁:一种【锁竞争机制】,不是锁的状态(前面几个都是锁的状态)

  • 线程的阻塞和唤醒需要CPU从用户态转为核心态,频繁的阻塞和唤醒对CPU来说是一件负担很重的工作。为了避免这种情况,所以就有了自旋锁。其实也就是 CAS。
  • 所谓自旋锁,就是指当一个线程尝试获取某个锁时,如果该锁已被其他线程占用,就一直循环检测锁是否被释放,而不是进入线程挂起或睡眠状态。
  • 虽然它可以避免线程切换带来的开销,但是它占用了CPU处理器的时间。

适用范围:

执行时间短(加锁代码),锁的粒度小,线程数少,用自旋

执行时间长,线程数多,用系统锁

补充 适应性自旋锁 1.6开始引入

    • 线程如果自旋成功了,那么下次自旋的次数会更加多,因为虚拟机认为既然上次成功了,那么此次自旋也很有可能会再次成功,那么它就会允许自旋等待持续的次数更多。
    • 如果对于某个锁,很少有自旋能够成功,那么在以后要或者这个锁的时候自旋的次数会减少甚至省略掉自旋过程,以免浪费处理器资源。

我们再来回顾下我们的对象头,然后通过一个图来了解下轻量级锁及锁膨胀的流程。

一个标记位专门标记 偏向锁 or 无锁 状态。

一个标记位专门标记 轻量级锁 or 重量级锁 状态。

补充说明:

  1. JVM会为每个线程在当前线程的栈帧中创建用于存储锁记录的空间,官方称为Displaced Mark Word。
  2. 第一次CAS尝试获取锁。若一个线程获得锁时发现是轻量级锁,会把锁的MarkWord复制到自己的Displaced Mark Word里面。然后线程尝试用CAS将锁的MarkWord替换为指向锁记录的指针。如果成功,当前线程获得锁.
  3. 执行完同步块,第二次CAS,当前线程会使用CAS操作将Displaced Mark Word的内容复制回锁的Mark Word里面。如果没有发生竞争,那么这个复制的操作会成功。如果有其他线程因为自旋多次导致轻量级锁升级成了重量级锁,那么CAS操作会失败(这里失败的是尝试将Displaced Mark Word的内容复制回锁的Mark Word里面),CAS失败后,就会进入重量级锁的解锁流程,即找到monitor对象,把owner设置为null,唤醒entrylist中的blocked线程。

最后,我们对这几种锁进行一个对比,从而帮助我们从全局的角度更好地了解其特性

下表来自《Java并发编程的艺术》:

优点

缺点

适用场景

偏向锁

加锁和解锁不需要额外的消耗,和执行非同步方法比仅存在纳秒级的差距。

如果线程间存在锁竞争,会带来额外的锁撤销的消耗。

适用于只有一个线程访问同步块场景。

轻量级锁

竞争的线程不会阻塞,提高了程序的响应速度。

如果始终得不到锁竞争的线程使用自旋会消耗CPU。

追求响应时间。同步块执行速度非常快。

重量级锁

线程竞争不使用自旋,不会消耗CPU。

线程阻塞,响应时间缓慢。

追求吞吐量。同步块执行时间较长。

关键点:竞争多不多,开销大不大。

总结

本文是Synchronized解析的最后一篇,主要对锁升级的过程进行了描述,解析了从偏向锁到重量级锁的各阶段状态的锁及自旋的锁竞争机制。最后对不同状态的锁进行了比较,相信近期的这4篇文章,能够帮助我自己和大家加深对Synchronized的了解。欢迎大家留言,讨论,提建议。

更多更精彩的内容请关注。

V信公众号搜索 “程序员一棵树”。 内有相关文档原文和免费资料。

D音搜索 “程序员一棵树”。

B站搜索 “程序员一棵树”。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员一棵树

创作不易,感谢土豪打赏

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值