你要的synchronized锁升级与降级这里都有

1.1 锁实现和升级

monitorenter和monitorexit是如何实现获取锁和释放锁的。可以看到synchronized关键字在修饰代码块的时候,会跟上一个 SynchronizedTestDemo02.class,在修饰静态方法时,其实也隐式的包含了一个类的Class对象。如果修饰的非静态方法,则隐式的包含了this,代表当前调用非静态方法的对象。

可以看出,锁的获取和释放应该和这些对象有关系。究其原因,是每个对象的对象头,都包含锁相关的信息。每个Java对象包含对象头、实例数据、对齐填充自己三部分。其中对象头中的Mark Word和锁密切相关。Mark Word的长短和JVM的位数有关,32位JVM中Mark Word的长度为32,,64位JVM中Mark Word的长度为64。

从上图可以看出,对象头分为三个部分:Mark Word、指向类的指针、数组长度。

Mark Word:该字段可以用来记录对象被作为锁用途时的状态。

  1. 无锁:如果一个对象没有被作为锁用途,即没有出现在 synchronized关键字后面,这个对象是无锁状态,Mark Word字段主要用来记录对象的HashCode,分代年龄、是否偏向锁、锁标志位。其中分代年龄表示Young GC次数,最大值15,超过则晋升老年代。
  2. 偏向锁:当一个线程过来获取锁的时候,首先检测其锁标志位,如果是01的话,再检测其是否是偏向锁,如果此时是无锁状态,则使用CAS机制将当前对象头的偏向锁(线程id)指向当前线程,并将是否偏向锁置为1。如果对象头已经是偏向锁时, 此时,如果新来的线程和已经获得偏向锁的线程是同一个线程,则直接执行。如果不是同一个线程,则尝试将对象头的偏向锁线程id指向新的线程。

偏向锁的撤销:偏向锁采用一种直到出现竞争才释放锁的机制,也就是线程A拿到偏向锁后,没有其他线程与其竞争的情况下,偏向锁将一直指向线程A,即使线程A已经从同步块退出甚至消亡,这样下次有新的任务要执行时,线程A在检测Mark Word的偏向锁线程id没有变化后,可以直接执行新的任务。这样在线程B过来竞争锁的时候,就会出现两种情况。如果线程A仍然在同步块中,则线程B无法竞争到锁,此时会触发撤销偏向锁,升级为轻量级锁。如果线程A已经不在同步块中,这时可以撤销线程A的偏向锁,将Mark Word的偏向锁线程id设置为线程B的id。

如果线程A还在同步块中:

(1)线程B发现线程A拿到了偏向锁,线程A还在同步块中

(2)线程A暂停,保存一个全局安全点(STW),准备撤销偏向锁。首先将偏向锁标志标记为0,并将锁状态标记为00;然后在线程A的栈桢中开辟一个区域,存储对象头中Mark Word的位置信息,并存储了一个Mark Word副本(无锁状态的Mark Word),官方称之为Displaced Mark Word;最后通过CAS在对象头Mark Word中存储指针指向线程A栈桢中存储无锁状态Mark Word位置信息的区域(也叫锁记录,除了无锁状态Mark Word以外,还有指向对象的指针、标记锁记录是否有效的标志位)。

(3)偏向锁升级为轻量级锁以后,因为线程A仍在同步块中,所以仍是线程A优先持有锁,线程A继续执行代码。

(4)线程B会通过CSA进行自旋,尝试获取锁,若自旋失败,最终会升级为重量级锁,这时线程B被放到阻塞队列中。

如果线程A不在同步块中:

(1)线程B发现线程A拿到了偏向锁,线程A已经不在同步块中

(2)线程A暂停,保存一个全局安全点(STW),撤销偏向锁,更新对象头的Mark Word为无锁。线程B会尝试使用CAS将Mark Word的线程id改为线程B

(3)可以看出偏向锁的撤销也是消耗一定性能的,如果一个类的各个对象的偏向锁被频繁的撤销,对系统的性能也有影响,因此,JVM设置了一个阈值,当超过这个阈值的时候,会触发批量重偏向,允许基于该类对象的重偏向直接切换为新线程,而不用撤销。Mark Word中的Epoch字段与此有关。

(4)如果批量重定向的阈值仍被突破,JVM会禁止该类创建的对象生成偏向锁,后续新对象直接从无锁状态升级为轻量级锁(上面提到的两个阈值都可以通过虚拟机参数进行设置。

注意:单个对象如果经历了从偏向锁到轻量级锁升级后,后续即使从轻量级锁降级为无锁状态的话,也不会再经历偏向锁了,对这个对象来说,偏向锁已经被禁用了。

  1. 轻量级锁:轻量级锁是一个过渡状态,适用于低竞争的场景(两个线程交替执行代码块)

轻量级锁升级为重量级锁的触发条件

(1)在线程A获得轻量级锁的情况下,线程B通过自旋等待获取锁,自旋一段时间后,如果一直没有获取到锁,就会将锁升级为重量级锁,线程B被阻塞住。JVM会根据历史竞争情况动态调整自旋次数,可以通过-XX:+UseSpinning参数设置。

(2)竞争的线程数增加,如果又来了线程C竞争锁,会快速升级为重量级锁

(3)线程内部调用了wait()、notify()等方法,这些方法需要重量级锁支持

轻量级锁升级为重量级锁的过程:

(1)JVM为锁对象分配一个ObjectMonitor类的对象,这是C++代码,其中包含了阻塞队列、持有线程等字段,将Mark Word更新为指向ObjectMonitor的指针,锁的标识位变成10

(2)线程A原本的轻量级锁记录会被替换为指向ObjectMonitor对象的指针,线程A成为ObjectMonitor对象的持有者,ObjectMonitor对象中的_owner字段指向线程A

(3)线程B的CAS自旋失败后,调用操作系统同步原语挂起自己,线程B被放入到ObjectMonitor对象的阻塞队列中,等待被唤醒。

(4)线程A执行完monitorexit时,通过ObjectMonitor唤醒阻塞队列中的线程,如线程B。

线程A在将锁升级为重量级锁的过程中,其他线程会来干扰吗?讲道理此时线程A还持有者轻量级锁,其他线程过来竞争也是没什么用的。

  1. 重量级锁 :重量级锁是synchronized锁的最终状态,一旦锁变成重量级锁,来了一个新线程,通过自旋,获取不到锁的情况下,便会进入线程阻塞队列,该线程从操作系统层面被挂起,并且从用户态进入内核态;当线程A退出同步块的时候,JVM会从线程阻塞队列挑选一个线程唤醒(通常是队首线程),并将ObjectMonitor对象中的_owner字段指向被唤醒的线程,被唤醒的线程获取锁,从内核态转为用户态,然后开始执行。当然也存在竞争的情况,当通过Object.notifyAll()同时唤醒多个线程时,这多个被唤醒的线程就会竞争锁。重量级锁种的内核态的频繁切换是导致其性能低下的一个主要原因。

综上,synchronized锁的实现严重依赖JVM,JVM在背后做了很多工作,Java是解释执行语言,当JVM遇到monitorenter字节码指令时,会在逻辑中,插入锁管理相关的代码逻辑,用以控制锁的控制和升级。JVM屏蔽了很多袭击,虽然让用户使用起来比较简单,但也导致了一个问题:缺乏灵活性和扩展性,毕竟没有几个人有能力去修改JVM的代码。所以Java社区后续又发明了Lock(ReentrantLock、ReentrantReadWirteLock等),它们是在Java语法层面实现的,大家花点时间研究后,甚至可以定义出自己的锁。

  1. GC标记,当对象被垃圾回收器标记为可回收时,锁标识位被标记为11,其他位被置空。

2.2. 锁降级

重量级锁不会再降级成其他锁,因为重量级锁实现复杂,降级成本高,综合下来,降级收益并不明显。JVM没有提供重量级锁降级的方式。

轻量级锁可以降级到无锁(不会降级到偏向锁), HotSpot 中轻量级锁降级的机制是:当线程A从同步块退出时,此时不会立即执行降级操作,对象头的Mark Word和线程A的栈桢锁记录仍然保持轻量级锁标记。直到线程B过来竞争锁,JVM通过CAS机制将Mark Word设置为无锁标记,但是随着线程B获取锁,Mark Word会马上升级为轻量级锁。

这里有个疑问,这种情况下为什么不直接降级到偏向锁?

  • Mark Word一旦经历从偏向锁到轻量级锁,则会认为当前存在线程竞争,会禁用偏向锁,所以线程B直接从无锁到轻量级锁了,偏向锁的目的是在没有竞争的场景下完全避免CAS
  • 偏向锁Mark Word中存储的是Thread Id和Epoch,持有轻量级锁的线程栈桢锁记录中存储的是无锁状态的Mark Word,可以直接复制到对象头的Mark Word中,更加方便快速。

HotSpot 中偏向锁降级到无锁也和上面类似,线程A从同步块中退出时,JVM不会立即对锁进行降级,而是当有线程B过来竞争偏向锁的时候,JVM才会将Mark Word的偏向锁先撤销,降级到无锁,随着线程B竞争到锁,Mark Word又会立即升级为偏向锁。

下面是锁变化过程的示意图:

1.3. 总结

  • 偏向锁是一种中间过渡锁,对于一个对象来说,一旦从偏向锁升级到轻量级锁,将不再经历偏向锁

  • 对于一个类来说,如果基于该类创建的对象频繁经历偏向锁切换,超过一定阈值,该类创建的对象将不允许使用偏向锁

  • 偏向锁状态下,在线程A已经退出同步块的情况下,线程B过来竞争锁,其实不存在锁竞争

  • 轻量级锁下,线程A还在同步块中,线程B自旋竞争锁,在自旋结束前,线程B获取到锁,这种情况下,尚且升级不到重量级锁

为什么需要先撤销偏向锁?

  • 偏向锁的设计目标是 单线程无竞争场景,一旦多线程竞争,JVM 需要回退到更通用的轻量级/重量级锁机制。
  • 撤销偏向锁是为了 保证线程安全,避免两个线程同时误判锁状态
相关推荐
零千叶16 分钟前
【面试】AI大模型应用原理面试题
java·设计模式·面试
坐吃山猪5 小时前
SpringBoot01-配置文件
java·开发语言
我叫汪枫5 小时前
《Java餐厅的待客之道:BIO, NIO, AIO三种服务模式的进化》
java·开发语言·nio
yaoxtao5 小时前
java.nio.file.InvalidPathException异常
java·linux·ubuntu
Swift社区7 小时前
从 JDK 1.8 切换到 JDK 21 时遇到 NoProviderFoundException 该如何解决?
java·开发语言
DKPT8 小时前
JVM中如何调优新生代和老生代?
java·jvm·笔记·学习·spring
phltxy8 小时前
JVM——Java虚拟机学习
java·jvm·学习
seabirdssss9 小时前
使用Spring Boot DevTools快速重启功能
java·spring boot·后端
喂完待续9 小时前
【序列晋升】29 Spring Cloud Task 微服务架构下的轻量级任务调度框架
java·spring·spring cloud·云原生·架构·big data·序列晋升