【并发编程学习篇】synchronized的原理

文章详细介绍了Synchronized的原理,包括在JDK1.6之前的重量级锁以及之后引入的锁升级机制,如偏向锁和轻量级锁,以平衡性能和安全性。同时,解释了锁的状态记录、偏向锁的获取过程和撤销条件,以及轻量级锁和重量级锁的获取过程。此外,还讨论了锁粗化和锁消除等优化策略,这些是通过逃逸分析实现的,以提高程序性能。
摘要由CSDN通过智能技术生成

一、Synchronized 原理是什么

  1. 在jdk1.6之前synchronized重量级锁,基于Monitor(管程是指管理共享变量以及对共享变量操作的过程,让它们支持并发)机制实现,依赖底层操作系统的Mutex Lock互斥锁实现的

  2. 由于权限隔离的关系,应用程序去调用系统方法的时候需要切换到内核状态去执行,这样就涉及到了用户态到内核态的切换,这个切换对性能有较大的影响

  3. 所以在jdk1.6之后呢,synchronized增加了锁升级的机制来平衡数据安全性和性能。synchronized会根据线程竞争的情况,先去尝试在不加重量级锁的情况下去保证线程的安全性,所以引入了偏向锁和轻量级锁的机制

Synchronized 是由 JVM 实现的一种实现互斥同步的一种方式,如果你查看被 Synchronized 修饰过的程序块编译后的字节码,会发现,被 Synchronized 修饰过的程序块,在编译前后被编译器生成了 monitorenter 和 monitorexit 两个字节码指令。

这两个指令是什么意思呢?

  1. 在虚拟机执行到 monitorenter 指令时,首先要尝试获取对象的锁: 如果这个对象没有锁定,或者当前线程已经拥有了这个对象的锁,把锁的计数器 +1;当执行 monitorexit 指令时将锁计数器 -1;
  2. 当计数器为 0 时,锁就被释放了。 如果获取对象失败了,那当前线程就要阻塞等待,直到对象锁被另外一个线程释放为止。

锁的状态是如何记录的?

对象头Mark Work2bit 来记录锁的状态标位,由于无锁和偏向锁的锁标识都是 01,没办法区分,这里引入 1bit 的偏向锁标识位来记录是否偏向锁

二、偏向锁原理,为什么引入偏向锁

  1. 大多数情况下,锁不仅不存在多线程竞争,而且总是由同一线程多次获得,如果每次都要竞争锁会增大很多没有必要付出的代价,为了降低获取锁的代价,才引入的偏向锁。
  2. 对于没有锁竞争的场合,偏向锁有很好的优化效果。

2.1 偏向锁如何获取的?

  1. 在偏向锁状态的时候,对象头的 Mark Word 中记录了当前偏向的 Thread Id
  2. 首先会先获取 Mark Word 中记录的偏向的Thread Id,判断当前对象是否处于可偏向状态(即当前没有对象获得偏向锁)。
  3. 如果是可偏向状态,则通过 CAS 操作,把当前线程的ID,写入到 Mark Word 如果成功,表示获得偏向锁成功
  4. 如果CAS失败则说明当前有其他线程获得了偏向锁,这时候就需要将已获得偏向锁的线程执行 偏向锁撤销,并升级为轻量级锁
  5. 偏向锁的撤销需要修改 Mark Word 中的锁状态标志位为无锁,并且需要等待全局安全点,即在这个时间点上没有正在执行的字节码(有性能问题)。

当多个线程争夺一个资源时,JVM 会尝试用偏向锁来提高效率。偏向锁的原理是:当一个线程获得锁时,它将会“偏向”到该线程,并在后续竞争时,会优先分配给这个线程,从而避免线程竞争,但如果某个线程竞争竞争到了锁,而该线程未被偏向,则 JVM 会撤销该锁,并重新分配锁,这会导致性能下降。

三、轻量级锁的获取锁过程

  1. 首先会判断当前是否为无锁状态,也就是Mark Work中是否偏向的标志位为0

  2. 如果是的话开始加锁操作,加锁的的过程首先是将Mark Word 拷贝一份到线程栈,紧接着利用CAS操作尝试更新Mark Word 中指向栈中锁记录的指针的指向

  3. 如果更新成功,则表示成功获取锁,否则直接开始执行锁膨胀(这里轻量级锁没有自旋的操作)

四、重量级锁获取锁的过程

  1. 在重量级的获取的时候会使用 CAS自旋 的方式获取锁,直到自旋到一定的次数以后开始执行入队操作(_crq队列),入队成功后也会再次尝试是否能够获取锁
  2. 如果获取失败,才真正的将线程 挂起,这个反复的CAS自旋的方式去尝试获取锁是为了尽可能的减少 用户态内核态 的操作

五、锁粗化

  1. 假设一系列的连续操作都会对同一个对象反复加锁及解锁,甚至加锁操作是出现在循环体中的,即使没有出现线程竞争,频繁地进行互斥同步操作也会导致不必要的性能损耗。
  2. 如果JVM检测到有一连串零碎的操作都是对同一对象的加锁,将会 扩大加锁同步的范围(即锁粗化)到整个操作序列的外部。
StringBuffer buffer = new StringBuffer();
/**
 * 锁粗化
 */
public void append(){
    buffer.append("aaa").append(" bbb").append(" ccc");
}
  1. 上述代码每次调用 buffer.append 方法都需要加锁和解锁,如果JVM检测到有一连串的对同一个对象加锁和解锁的操作,就会将其合并成一次范围更大的加锁和解锁操作
  2. 即在第一次append方法时进行加锁,最后一次append方法结束后进行解锁

六、锁消除

锁消除即删除不必要的加锁操作。锁消除是Java虚拟机在JIT编译期间,通过 逃逸分析去除不可能存在共享资源竞争的锁,通过锁消除,可以节省毫无意义的请求锁时间。

逃逸分析(Escape Analysis)

  1. 逃逸分析,是一种可以有效减少Java 程序中同步负载和内存堆分配压力的跨函数全局数据流分析算法。
  2. 通过逃逸分析,Java Hotspot编译器能够分析出一个新的 对象的引用的使用范围 从而决定是否要将这个对象分配到堆上。逃逸分析的基本行为就是分析对象动态 作用域

方法逃逸(对象逃出当前方法)

当一个对象在方法中被定义后,它可能被外部方法所引用,例如作为调用参数传递到其他地方中。

线程逃逸((对象逃出当前线程)

这个对象甚至可能被其它线程访问到,例如赋值给类变量或可以在其它线程中访问的实例变量。

使用逃逸分析,编译器可以对代码做如下优化:

  1. 锁粗化或锁消除:如果一个对象被发现只能从一个线程被访问到,那么对于这个对象的操作可以不考虑同步。

  2. 将堆分配转化为栈分配:如果一个对象在子程序中被分配,要使指向该对象的指针永远不会逃逸,对象可能是栈分配的候选,而不是堆分配。

  3. 分离对象或标量替换:有的对象可能不需要作为一个连续的内存结构存在也可以被访问到,那么对象的部分(或全部)可以不存储在内存,而是存储在CPU寄存器中。

jdk6才开始引入该技术,jdk7开始默认开启逃逸分析。在Java代码运行时,可以通过JVM参数指定是否开启逃逸分析:

-XX:+DoEscapeAnalysis  //表示开启逃逸分析 (jdk1.8默认开启)
-XX:-DoEscapeAnalysis //表示关闭逃逸分析。 
-XX:+EliminateAllocations //开启标量替换(默认打开) 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

Java学习者柯十一

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值