介绍一下synchronized锁升级过程
分析:
首先介绍一下synchronized
synchronized 是 Java 中用于实现线程同步的关键字、它通过一种称为锁升级的机制来优化性能、根据锁的竞争情况动态调整锁的状态。
synchronized
的核心是基于 monitor
锁、也称为 监视器锁 或 互斥锁。monitor
是一种重量级的锁机制,它的实现依赖于操作系统提供的同步原语(如互斥量 mutex
)
Monitor 对象:
-
在 Java 中 每个对象都有一个与之关联的
monitor
它存储在对象的 对象头 中:
对象头通常包含以下内容:
-
Mark Word:存储对象的哈希码、锁状态、GC 标记等信息。
-
Class Metadata Address:指向对象的类元数据。
-
Array Length(如果是数组):存储数组的长度。
-
这个 Monitor 对象可以理解为一把锁。
-
Monitor 对象包含
_owner
(指向持有锁的线程),
-
monitor
锁有以下几种状态:
-
无锁状态:对象未被锁定。
-
偏向锁:偏向于一个线程、减少锁的开销。
-
轻量级锁:使用自旋锁、减少线程阻塞。
-
重量级锁:当自旋失败后、线程进入阻塞状态、使用操作系统级别的锁。
-
共享锁:允许多个线程同时访问(如读写锁的读锁)。
JDK 1.6 引入了锁升级机制 根据不同的竞争情况选择不同的锁:
默认就是无锁状态:对象没有被任何线程锁定。Mark Word 记录的是对象的哈希码等信息。
在大多数情况下、锁不仅不存在多线程竞争、而且总是由同一个线程多次获得。偏向锁就是为了优化这种场景。
后面引入了偏向锁
-
当一个线程(比如线程A)第一次进入一个
synchronized
代码块并尝试获取锁时、如果该对象是无锁状态、并且偏向锁是启用的(默认启用)、JVM 会将对象头的 Mark Word 中的锁标志位(表示偏向锁)、同时用 CAS 操作尝试将当前线程A的ID记录到 Mark Word 中-
如果成功: 线程A就持有了该对象的偏向锁。之后线程A再次进入与此锁相关的
synchronized
块时、只需要检查 Mark Word 中的线程ID是否是自己,如果是则无需任何额外的同步操作(如CAS)、直接执行同步代码、效率极高。这就是偏向锁的偏向之处。 -
如果失败 (CAS竞争或已有偏向):说明有其他线程竞争偏向锁、此时会尝试撤销偏向锁。撤销偏向锁后、锁会膨胀为轻量级锁。
-
然后到了轻量级锁
-
引入目的: 当偏向锁被撤销、或者多个线程交替执行同步代码块、但几乎没有实际的同时竞争时、使用轻量级锁可以避免重量级锁的操作系统内核态切换开销。
-
升级过程与判断:
-
尝试获取锁的线程(比如线程A)会在自己的栈帧中创建一个锁记录 (Lock Record)、用于存储对象 Mark Word 的一个拷贝(Displaced Mark Word)。
-
然后线程A会尝试使用 CAS ) 操作将对象头的 Mark Word 更新为指向其栈帧中这个锁记录的指针。同时锁记录中的
owner
指针会指向对象。 -
如果CAS成功: 线程A成功获取轻量级、Mark Word 的锁标志位会变为00
-
如果CAS失败: 说明在它尝试CAS期间、已经有其他线程先一步获取了轻量级锁
-
-
自旋等待:
-
获取轻量级锁失败的线程并不会立即阻塞、而是会进行自旋 、自旋锁是一种非阻塞式的锁实现方式、线程在尝试获取锁失败时不会进入阻塞或者挂起状态、而是通过循环自旋的方式等待锁的释放
-
JVM 会进行自适应自旋、即自旋的次数和时间会根据以往自旋成功的情况动态调整。
-
自旋锁可以避免线程切换的开销、如果锁被其他线程长时间占用、自旋线程会一直占用 CPU 资源、造成浪费。
-
-
锁膨胀:
-
如果自旋一定次数后仍然无法获取锁(例如持有锁的线程执行时间较长、或者有其他线程也在自旋竞争),轻量级锁就会膨胀为重量级锁。
-
重量级锁
-
引入场景: 当多个线程激烈竞争同一个锁、自旋等待无法有效解决问题时、就会升级到重量级锁。
-
实现: 重量级锁依赖于对象的 Monitor 实现、而 Monitor 又依赖于操作系统的互斥量 (Mutex)。
-
过程:
-
Mark Word 的锁标志位会变为1、并指向一个真正的 Monitor 对象
-
线程获取重量级锁失败后会被阻塞、并放入该 Monitor 的等待队列中、其状态会从运行态切换到阻塞态、等待操作系统进行线程调度和唤醒。
-
当持有锁的线程释放锁时、会唤醒等待队列中的一个或多个线程来竞争锁。
-
-
开销: 重量级锁涉及用户态到内核态的切换以及线程的阻塞和唤醒、开销较大。
总结锁升级路径:
无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁
这个过程是单向的、通常只能升级、不能降级
适用场景如下:
- 偏向锁: 适用于只有一个线程访问同步代码块的场景。 线程第一次获取锁时、会将对象头设置为偏向模式、后续该线程再次进入同步块时、无需进行任何同步操作、从而避免了额外的开销。
- 轻量级锁: 适用于多个线程交替访问同步代码块、但竞争不激烈的场景。
- 重量级锁线程的竞争不使用自旋、不会消耗CPU但是重量级的开销比较大、因为涉及到用户态和内核态的切换。响应时间慢
参考面试回答
介绍一下synchronized锁升级过程
首先介绍一下synchronized
- synchronized 是 Java 中用于实现线程同步的关键字
synchronized
的核心是基于monitor
锁、也称为 监视器锁 或 互斥锁。monitor
是一种重量级的锁机制,它的实现依赖于操作系统提供的同步原语(如互斥量mutex
)- Monitor 对象: 是在 Java 中 每个对象都有一个与之关联的
monitor
它存储在对象的 对象头 中: - 锁的状态记录在对象头里的(Mark Word)中
为提升性能、JVM 根据竞争情况引入了 锁的升级机制、一共有四种状态:
无锁 -> 偏向锁 -> 轻量级锁 -> 重量级锁
过程如下:
默认是无锁状态、Mark Word 存储哈希值等元信息、无线程持有锁。
然后到了偏向锁
-
当线程第一次获取锁时、JVM 会将该线程ID写入对象头的 Mark Word 中。
-
之后该线程再次进入同步块时、只需比对线程ID,无需加锁操作,效率极高。
-
当其他线程尝试获取锁时、偏向锁会被撤销、进入锁升级流程。
升级到轻量级锁
-
线程尝试加锁时、会复制对象头的 Mark Word 到自己的栈中叫锁记录
-
然后尝试使用 CAS 将对象头指向自己的锁的记录、表示自己持有锁。
-
如果 CAS 失败:说明有其他线程已持有锁 → 自旋尝试获取锁(避免线程阻塞)。
-
如果自旋一定次数后仍然无法获取锁(例如持有锁的线程执行时间较长、或者有其他线程也在自旋竞争)、轻量级锁就会膨胀为重量级锁。
✅ 重量级锁
-
自旋失败后,锁膨胀为重量级锁、线程会被阻塞挂起、等待操作系统调度。
-
对象头的 Mark Word 指向 Monitor 对象,Monitor 会维护一个等待队列。
-
开销大、涉及用户态与内核态切换、响应慢。适用场景: 多线程同时竞争锁