在 Java 中,synchronized
锁的实现包含三种状态:偏向锁、轻量级锁和重量级锁。偏向锁是一种优化机制,目的是为了减少同一线程重复获取锁的开销,尤其是在无竞争的情况下。偏向锁是“偏向”第一次获取锁的线程,以减少加锁和解锁的代价。
偏向锁的机制和偏向的对象
偏向锁阶段会偏向于第一个获取锁的线程。具体来说:
- 偏向锁的概念:偏向锁的主要目的是在单线程环境下减少锁操作的成本。默认情况下,偏向锁在 JVM 启动时是开启的(可以通过
-XX:+UseBiasedLocking
启用,JDK 15 后偏向锁已被弃用)。偏向锁的特点是锁定的对象会“偏向”于第一个获取该锁的线程。 - 偏向的对象:偏向锁并非立即执行加锁操作,而是将锁的“偏向”信息记录在对象头和当前线程中。当第一个线程访问加锁的代码块时,对象会偏向该线程。随后,同一线程再次进入该代码块时,不需要再次执行同步检查,直接进入临界区。
偏向锁的具体工作流程
偏向锁的实现依赖于对象头(Mark Word)中的标志位。当对象第一次被某个线程获取锁时,会将该线程的 ID 记录在对象头中,表示该对象已经“偏向”该线程。具体流程如下:
- 线程初次获取锁,设置偏向:
- 当一个线程首次尝试获取对象锁时,JVM 会检查对象头的状态位,如果该对象还没有被偏向(无锁状态),则将线程 ID 写入对象头,标记该线程对该对象拥有偏向锁。偏向锁的状态会标记在对象的 Mark Word 中。
- 随后,同一线程再次尝试进入同步代码块时,不需要执行任何同步操作,因为对象已经偏向该线程了。这减少了锁重入的开销。
- 无锁竞争的场景,偏向锁保持:
- 偏向锁的优势在于无锁竞争的场景:只要该对象没有出现其他线程的锁竞争,偏向锁的线程可以不进行加锁和解锁操作而直接进入临界区。
- 偏向锁的撤销只会在其他线程尝试获取该锁时才发生,而不会在同一线程中撤销。
- 偏向锁撤销:
- 当另一个线程尝试获取该偏向锁对象时,JVM 会撤销偏向锁,将其升级为轻量级锁。这个过程会暂停偏向锁持有线程,撤销偏向锁状态,恢复对象到无锁或轻量级锁的状态。
- 偏向锁的撤销会触发一次 CAS(Compare-And-Swap)操作,将锁升级为轻量级锁,以便支持多线程的并发访问。
偏向锁的 Mark Word 结构
在偏向锁状态下,对象头的 Mark Word 会记录以下信息:
- 锁标志位:标记当前锁的状态(偏向锁、轻量级锁、重量级锁等)。
- 偏向标志位:标记对象是否是偏向锁状态。
- 线程 ID:保存偏向锁持有线程的 ID。
偏向锁状态下的 Mark Word 结构如下:
偏向锁标志 | 锁标志 | 线程 ID (或偏向线程 ID) |
---|---|---|
1 | 01 | 记录偏向的线程 ID |
偏向锁的优缺点
优点:
- 在无锁竞争的情况下,偏向锁能显著减少锁的获取和释放的开销,因为它避免了 CAS 操作。
- 偏向锁适合于线程局部使用的锁对象,例如大多数情况下由同一线程独占的锁对象。
缺点:
- 如果存在多个线程竞争锁,偏向锁会增加锁撤销的开销。在多线程高竞争环境下,偏向锁的撤销可能会导致性能下降。
- 可以在高并发环境中通过 JVM 参数
-XX:-UseBiasedLocking
禁用偏向锁,从而直接使用轻量级锁。
举例说明偏向锁的工作流程
假设有以下代码片段:
javaCopy codepublic class Example {
private final Object lock = new Object();
public void synchronizedMethod() {
synchronized (lock) {
// 临界区代码
}
}
}
偏向锁流程示例
- 首次加锁:当线程
T1
首次进入synchronizedMethod
时,lock
对象的状态由无锁变为偏向锁状态。JVM 将T1
的线程 ID 写入lock
对象的 Mark Word 中,表示此对象偏向T1
。 - 重复加锁:如果
T1
再次进入synchronizedMethod
,因为lock
已经偏向T1
,它可以直接进入临界区,而不需要执行任何同步操作。 - 其他线程竞争:如果线程
T2
尝试进入synchronizedMethod
,JVM 将撤销lock
的偏向锁,将其升级为轻量级锁。偏向锁的撤销涉及 CAS 操作,并暂停T1
线程以更新对象头信息。 - 进一步的竞争:如果锁的竞争持续,轻量级锁可能进一步升级为重量级锁,导致进入重量级锁的监视器锁定机制。
总结
偏向锁通过偏向于首次获取锁的线程,避免了不必要的加锁和解锁操作,极大提高了无锁竞争情况下的性能。但是在多线程并发环境中,如果偏向锁被频繁撤销,会带来性能损耗,因此在高竞争环境下通常不适用。偏向锁的设计适合于轻量级、无竞争的锁场景。