浅析 synchronized 锁升级流程
本文最后更新于 2025年8月11日 12:01
在 JDK 1.5 版本(包含)之前,锁的状态只有两种:“无锁状态”和“重量级锁状态”。只要有线程访问共享资源对象,锁就直接成为重量级锁。从 JDK 1.6 版本开始,JVM 对 synchronized
锁进行了优化,引入了“偏向锁”和“轻量级锁”,以减少上下文切换并提高性能。因此,锁的状态变为四种:
无锁(No Lock)
当共享资源没有多线程竞争访问时,不需要加锁,也就是无锁状态。
偏向锁(Biased Locking)
当共享资源首次被访问时,JVM 会在对象头中设置偏向锁标志,并将线程ID设置为当前线程的ID(操作系统的线程ID)。此时,后续如果同一个线程再次访问该共享资源,它会根据偏向锁标识与线程ID进行比对,若相同,直接获取锁,进入临界区,这也实现了
synchronized
锁的可重入性。轻量级锁(Lightweight Locking)
当多个线程竞争共享资源时,JVM 就会尝试使用轻量级锁,通过 CAS 进行自旋加锁,避免阻塞线程并通过循环等待获取锁。如果自旋超时且未成功,则会将锁升级为重量级锁。
重量级锁(Heavyweight Locking)
如果锁已经被某个线程持有(此时是偏向锁状态),在未释放锁前再有其他线程来竞争时,则会升级到重量级锁。另外,在轻量级锁状态下,多线程竞争锁时,也会升级到重量级锁。重量级锁由操作系统来实现,所以性能消耗相对较高。
锁的存放位置
每一个 Java 对象都有一个对象头,64 位 Java 虚拟机中的一个字宽是 64 位,32 位 Java 虚拟机中一个字宽是 32 位,非数组对象的对象头占用两个字宽,数组对象的对象头占用三个字宽(其中一个字宽用来存储数组的长度)。
偏向锁
大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得,于是引入了偏向锁。
偏向锁会偏向于第一个访问锁的线程,如果在接下来的运行过程中,该锁没有被其他的线程访问,则持有偏向锁的线程将永远不需要触发同步。
实现原理
首次访问时(线程 A):
- 当 线程 A 首次访问同步代码块时,JVM 会检查对象头的
MarkWord
,发现锁没有被占用(或者是空闲状态),于是线程 A 会 将自己的线程 ID 写入到对象的MarkWord
中,表示该锁现在偏向于线程 A。 - 此时,
MarkWord
中会保存线程 A 的线程 ID,表示此对象的锁是偏向于 A 线程的。 - 线程 A 后续再次进入该同步代码块时,JVM 会直接从
MarkWord
中读取线程 ID,发现是自己,就不需要再进行加锁操作。
后续访问时(线程 B):
- 当 线程 B 访问该同步代码块时,JVM 会检查
MarkWord
,发现该锁已经偏向了线程 A(即MarkWord
中保存的是线程 A 的线程 ID)。 - 由于线程 B 的线程 ID 与
MarkWord
中的线程 ID 不同,JVM 会 触发偏向锁的撤销,并将锁状态升级为 轻量级锁 或 重量级锁(通常是轻量级锁,具体依赖于 JVM 的实现和锁竞争的情况)。 - 撤销偏向锁时,JVM 会通过 CAS 操作,尝试将对象头中的
MarkWord
更新为 轻量级锁 或 重量级锁 的状态。 - 线程 B 会进入 自旋 或 阻塞,尝试竞争该锁。
轻量级锁
多个线程在不同时段获取同一把锁,即不存在锁竞争的情况,也就没有线程阻塞。针对这种情况,JVM 采用轻量级锁来避免线程的阻塞与唤醒。
具体来说,偏向锁升级为轻量级锁的过程大致分为以下几个步骤:
线程 A 获取偏向锁
- 线程 A 第一次访问某个同步代码块时,JVM 会检查对象头中的 Mark Word。
- 由于此时没有其他线程竞争,JVM 会将 Mark Word 更新为线程 A 的 ID,从而标记该对象为偏向锁状态,并允许线程 A 进入同步代码块。
线程 B 尝试获取偏向锁
当线程 B 尝试访问相同的同步代码块时,JVM 会首先检查 Mark Word,发现该对象的锁已经被线程 A 持有(通过线程 ID 判断)。
偏向锁撤销与轻量级锁创建
撤销偏向锁:由于线程 B 发现当前锁为偏向锁,并且该偏向锁并未偏向自己,JVM 会尝试 撤销偏向锁,并将锁升级为轻量级锁(并不一定会升级成功)。
Mark Word 进入轻量级锁状态:JVM 会把对象头中的 Mark Word 中存储的线程 ID 清除。对象头的 Mark Word 的标志位将会被标记为轻量级锁。然后,JVM 会使线程 B 尝试用 CAS 操作 把 Mark Word 的内容(存储原来的锁状态,如分代年龄、CMS_free、hashCode等固有属性)复制到当前线程的栈帧中的 锁记录(Lock Record) 区域,而对象头中则存储一个指向该区域的指针。
自旋和竞争
如果多个线程在此时尝试获取锁(比如线程 C 也尝试获取该锁),JVM 会继续使用 CAS 操作来竞争该锁。
- 如果 CAS 操作成功,则线程 C 获得锁并继续执行同步代码块。
- 如果 CAS 操作失败,说明线程 C 没有成功获得轻量级锁,此时轻量级锁会升级为 重量级锁,进入 阻塞状态,需要使用操作系统的互斥锁(Mutex)来保证同步。
重量级锁
一个线程在持有锁,一个线程在自旋,又有第三个线程来访问时,轻量级锁就会膨胀为重量级锁。重量级锁是指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。
重量级锁的效率很低,其底层是通过操作系统的 Mutex Lock 来实现的,每个对象指向一个 monitor对象,这个 monitor 对象在堆中与锁是关联的,通过 monitorenter
指令插入到同步代码块在编译后的开始位置,monitorexit
指令插入到同步代码块的结束处和异常处,这两个指令配对出现。JVM 的线程和操作系统的线程是对应的,重量级锁的 Markword 里存储的指针是这个 monitor 对象的地址,操作系统负责控制内核态中的线程的阻塞和恢复,从而达到 JVM 线程的阻塞和恢复。这个过程涉及内核态和用户态的切换,影响性能,所以叫重量级锁。
锁升级流程
检查偏向锁状态
首先,JVM 会检查对象的
MarkWord
是否包含当前线程的 ID。如果是,说明该锁处于 偏向锁 状态,当前线程可以直接获得锁,进入临界区。锁升级判断
如果
MarkWord
中存储的线程 ID 不是当前线程的 ID,说明当前线程无法直接获得锁,需要进行锁升级。在这种情况下,JVM 会通过 CAS 操作尝试将锁从 偏向锁 升级为 轻量级锁,并通知持有锁的线程进行挂起,同时将MarkWord
的内容设置为空。轻量级锁竞争
在锁升级为轻量级锁后,两个线程都将锁对象的
HashCode
复制到各自为锁创建的记录空间,并通过 CAS 操作尝试将MarkWord
的内容修改为各自记录空间的地址。这个过程是竞争性的,只有成功的线程才能获得锁。自旋
如果在第三步中,某个线程的 CAS 操作失败,它会进入 自旋 状态,不断尝试重新获取锁,直到成功获取锁或被唤醒。
自旋成功
如果自旋的线程在等待过程中成功获得锁(即之前持锁的线程释放了锁),则锁会保持在 轻量级锁 状态。自旋线程成功获取锁后,可以继续执行临界区代码。
升级为重量级锁
如果自旋超过一定次数仍然无法成功获取锁,则会触发 锁的升级,进入 重量级锁 状态。在此状态下,线程会被阻塞,直到之前的线程执行完毕并释放锁,唤醒自旋线程。
参考文章: