大白话讲解synchronized锁升级套路

宅哥聊构架 后端 2023-05-17

synchronized锁是啥?锁其实就是一个对象,随便哪一个都可以,Java中所有的对象都是锁,换句话说,Java中所有对象都可以成为锁。
这次我们主要聊的是synchronized锁升级的套路

synchronized会经历四个阶段:无锁状态偏向锁轻量级锁重量级锁 依次从耗费资源最少,性能最高,到耗费资源多,性能最差。

锁原理

先看看这些状态的锁为什么称之为锁,他们的互斥原理是啥。

偏向锁

当一个线程到达同步代码块,尝试获取锁对象的时候,会查看对象头中的MarkWord里的线程ID,如果这里没有ID则将自己的保存进去,拿到锁。若是有,则查看是否是当前线程,如果不是,就CAS尝试改,如果是,就已经拿到了锁资源。

这里详细说说CAS尝试修改的逻辑:它会检查持有偏向锁的线程状态。首先遍历当前JVM的所有存活的线程,如果能找到偏向的线程,则说明偏向的线程还存活,此时会检查线程是否在执行同步代码块中的代码,如果是,则升级为轻量级锁,去继续进行CAS竞争锁。所以加了偏向锁之后,同时只有一个线程可以拿到锁执行同步代码块中的代码。

轻量级锁

查看对象头中的MarkWord里的Lock Record指针指向的是否是当前线程的虚拟机栈,如果是,拿锁执行业务,如果不是则进行CAS,尝试修改,若是修改几次都没有成功,再升级到重量级锁。

重量级锁

查看对象头中的MarkWord里的指向的ObjectMonitor,查看owner是否是当前线程,如果不是,扔到ObjectMonitor里的EntryList中排队,并挂起线程,等待被唤醒。

锁升级

无锁

一般情况下,新new出来的一个对象,暂时就是无锁状态。因为偏向锁默认是有延迟的,在启动JVM的前4s中,不存在偏向锁,但是如果关闭了偏向锁延迟的设置,new出来的对象,就会添加一个匿名偏向锁。也就是说这个对象想找一个线程去增加偏向锁,但是没有找到,称之为匿名偏向。存储的线程ID为一堆0000,也没有任何地址信息。

我们可以通过以下配置关闭偏向锁延迟。

java
复制代码
//关闭偏向锁延迟的指令 -XX:BiasedLockingStartuoDelay=0

偏向锁

当某一个线程来获取这个锁资源时,此时会成功获取到,就会变为偏向锁,偏向锁存储线程的ID。

偏向锁升级时,会触发偏向锁撤销,偏向锁撤销需要等到一个安全点,比如GC的时候,偏向锁撤销的成本太高,所以默认开始时,会做偏向锁延迟。若是直接有多个线程竞争,会跳过偏向锁,直接变为轻量级锁。

细说一下偏向锁撤销的过程,成本为啥高呢?当一个线程拿到偏向锁之后,会把锁的对象头的Mark Work中的线程id指向自己,当又有一个线程来了进行争抢导致锁升级的的时候,会暂停之前拿到偏向锁的线程,然后清空Mark Work中的线程id增加一个轻量级锁,然后再恢复暂停的线程继续执行。这也是为什么等到安全点再执行锁升级的原因,因为要暂停线程。

常见的安全点:

  • 执行GC的时候
  • 方法返回之前
  • 调用某个方法之后
  • 抛出异常的位置
  • 一个循环的末尾

轻量级锁

当在出现了多个线程的竞争,就会升级为轻量级锁,轻量级锁的效果就是基于CAS尝试获取锁资源,这里会用到自适应自旋锁,根据上次CAS成功与否,耗费的时间,决定这次自旋多少次。


轻量级锁适用于竞争不是很激烈的场景,一个线程拿到锁,执行同步代码块,很快就处理完了。再来一个线程尝试一两次也拿到了锁,再去执行,不会让一个线程等待很久。

重量级锁

如果到了重量级锁,那就没啥说的了,如果有线程持有锁,其他想拿锁的就挂起,等待锁释放后被依次唤醒

锁粗化&锁消除

锁粗化/锁膨胀

锁膨胀是编译Java文件的时候,JIT帮我们做的优化,它会减少锁的获取和释放次数。比如:

java
复制代码
while(){ synchronized(){ // 多次的获取和释放,成本太高,会被优化为下面这种 } } synchronized(){ while(){ // 拿到锁后执行循环,只加锁和释放一次 } }

锁消除:

锁消除则是在一个加锁的同步代码块中,没有任何共享资源,也不存在锁竞争的情况,JIT编译时,就直接将锁的指令优化掉。比如

java
复制代码
synchronized(){ int a = 1; a++; //操作局部变量的逻辑 }
Apipost 私有化火热进行中

评论