浅析 synchronized 锁升级流程

本文最后更新于 2025年8月11日 12:01

在 JDK 1.5 版本(包含)之前,锁的状态只有两种:“无锁状态”和“重量级锁状态”。只要有线程访问共享资源对象,锁就直接成为重量级锁。从 JDK 1.6 版本开始,JVM 对 synchronized 锁进行了优化,引入了“偏向锁”和“轻量级锁”,以减少上下文切换并提高性能。因此,锁的状态变为四种:

  1. 无锁(No Lock)

    当共享资源没有多线程竞争访问时,不需要加锁,也就是无锁状态。

  2. 偏向锁(Biased Locking)

    当共享资源首次被访问时,JVM 会在对象头中设置偏向锁标志,并将线程ID设置为当前线程的ID(操作系统的线程ID)。此时,后续如果同一个线程再次访问该共享资源,它会根据偏向锁标识与线程ID进行比对,若相同,直接获取锁,进入临界区,这也实现了 synchronized 锁的可重入性。

  3. 轻量级锁(Lightweight Locking)

    当多个线程竞争共享资源时,JVM 就会尝试使用轻量级锁,通过 CAS 进行自旋加锁,避免阻塞线程并通过循环等待获取锁。如果自旋超时且未成功,则会将锁升级为重量级锁。

  4. 重量级锁(Heavyweight Locking)

    如果锁已经被某个线程持有(此时是偏向锁状态),在未释放锁前再有其他线程来竞争时,则会升级到重量级锁。另外,在轻量级锁状态下,多线程竞争锁时,也会升级到重量级锁。重量级锁由操作系统来实现,所以性能消耗相对较高。

image

锁的存放位置

每一个 Java 对象都有一个对象头,64 位 Java 虚拟机中的一个字宽是 64 位,32 位 Java 虚拟机中一个字宽是 32 位,非数组对象的对象头占用两个字宽,数组对象的对象头占用三个字宽(其中一个字宽用来存储数组的长度)。

image.png

偏向锁

大多数情况下锁不仅不存在多线程竞争,而且总是由同一线程多次获得,于是引入了偏向锁。

偏向锁会偏向于第一个访问锁的线程,如果在接下来的运行过程中,该锁没有被其他的线程访问,则持有偏向锁的线程将永远不需要触发同步。

实现原理

首次访问时(线程 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 获取偏向锁

  1. 线程 A 第一次访问某个同步代码块时,JVM 会检查对象头中的 Mark Word
  2. 由于此时没有其他线程竞争,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) 区域,而对象头中则存储一个指向该区域的指针。

image.png

自旋和竞争

如果多个线程在此时尝试获取锁(比如线程 C 也尝试获取该锁),JVM 会继续使用 CAS 操作来竞争该锁。

  • 如果 CAS 操作成功,则线程 C 获得锁并继续执行同步代码块。
  • 如果 CAS 操作失败,说明线程 C 没有成功获得轻量级锁,此时轻量级锁会升级为 重量级锁,进入 阻塞状态,需要使用操作系统的互斥锁(Mutex)来保证同步。

重量级锁

一个线程在持有锁,一个线程在自旋,又有第三个线程来访问时,轻量级锁就会膨胀为重量级锁。重量级锁是指当有一个线程获取锁之后,其余所有等待获取该锁的线程都会处于阻塞状态。

重量级锁的效率很低,其底层是通过操作系统的 Mutex Lock 来实现的,每个对象指向一个 monitor对象,这个 monitor 对象在堆中与锁是关联的,通过 monitorenter 指令插入到同步代码块在编译后的开始位置,monitorexit 指令插入到同步代码块的结束处和异常处,这两个指令配对出现。JVM 的线程和操作系统的线程是对应的,重量级锁的 Markword 里存储的指针是这个 monitor 对象的地址,操作系统负责控制内核态中的线程的阻塞和恢复,从而达到 JVM 线程的阻塞和恢复。这个过程涉及内核态和用户态的切换,影响性能,所以叫重量级锁。

锁升级流程

3230688-20231101142724469-1226844103.png

  • 检查偏向锁状态

    首先,JVM 会检查对象的 MarkWord 是否包含当前线程的 ID。如果是,说明该锁处于 偏向锁 状态,当前线程可以直接获得锁,进入临界区。

  • 锁升级判断

    如果 MarkWord 中存储的线程 ID 不是当前线程的 ID,说明当前线程无法直接获得锁,需要进行锁升级。在这种情况下,JVM 会通过 CAS 操作尝试将锁从 偏向锁 升级为 轻量级锁,并通知持有锁的线程进行挂起,同时将 MarkWord 的内容设置为空。

  • 轻量级锁竞争

    在锁升级为轻量级锁后,两个线程都将锁对象的 HashCode 复制到各自为锁创建的记录空间,并通过 CAS 操作尝试将 MarkWord 的内容修改为各自记录空间的地址。这个过程是竞争性的,只有成功的线程才能获得锁。

  • 自旋

    如果在第三步中,某个线程的 CAS 操作失败,它会进入 自旋 状态,不断尝试重新获取锁,直到成功获取锁或被唤醒。

  • 自旋成功

    如果自旋的线程在等待过程中成功获得锁(即之前持锁的线程释放了锁),则锁会保持在 轻量级锁 状态。自旋线程成功获取锁后,可以继续执行临界区代码。

  • 升级为重量级锁

    如果自旋超过一定次数仍然无法成功获取锁,则会触发 锁的升级,进入 重量级锁 状态。在此状态下,线程会被阻塞,直到之前的线程执行完毕并释放锁,唤醒自旋线程。


参考文章:


浅析 synchronized 锁升级流程
http://example.com/2024/11/20/浅析 synchronized 锁升级流程/
作者
Moonike
发布于
2024年11月20日
更新于
2025年8月11日
许可协议