Skip to content

JVM中锁升级的流程是什么

第一阶段:无锁状态 (No Lock)

一个Java对象刚被创建时,它不包含任何锁信息,其对象头中的锁标志位是01,称之为无锁状态。

第二阶段:偏向锁 (Biased Lock)

1. 升级时机

当第一个线程尝试获取这个对象的锁时,JVM并不会直接上重量级锁,而是会先将锁“偏向”给这个线程。这是基于一个经验假设:在大多数情况下,锁不仅不存在多线程竞争,而且总是由同一个线程多次获得。

2. 工作原理

  • 加锁过程:线程首次获取锁时,JVM会通过CAS(Compare-And-Swap)操作,尝试将该线程的ID记录在对象头(Mark Word)中,同时将锁标志位更新,状态位变为偏向锁状态。

  • 后续访问:当这个持有偏向锁的线程再次请求同一个锁时,它只需要检查对象头中记录的线程ID是否是自己。如果是,它就可以直接进入同步代码块,无需任何额外的同步操作,连CAS都不需要。这个过程的开销极低。

3. 锁的撤销与升级

偏向锁只有在出现其他线程尝试竞争时才会被撤销。

  • 当另一个线程(线程B)尝试获取这个已被线程A持有的偏向锁时,偏向锁的“偏向”模式就结束了。

  • JVM会检查原持有偏向锁的线程A是否仍然存活并且还在同步代码块中。

    • 如果线程A已不在同步块内:那么很简单,直接将对象头设置回无锁状态或偏向给新的线程B。

    • 如果线程A还在同步块内:说明产生了真正的竞争。这时JVM会暂停线程A,将锁升级为轻量级锁。这个过程被称为偏向锁的撤销(Revocation),它需要等待一个全局安全点(Global Safepoint),这个操作的开销相对较大。

注意:从JDK 15开始,偏向锁被默认禁用,并且预计在未来的版本中被移除。这是因为现代应用中,多线程竞争的场景越来越普遍,维护偏向锁的成本有时会超过其带来的收益。

第三阶段:轻量级锁 (Lightweight Lock)

1. 升级时机

当偏向锁被撤销,或者两个线程在几乎同一时间(但没有完全重叠)尝试获取同一个锁时,锁会升级为轻量级锁。它的设计目标是在没有实际多线程“同时”竞争的情况下,通过自旋(Spinning)来避免线程阻塞和唤醒带来的系统调用开销。

2. 工作原理

  • 加锁过程

    1. 线程在自己的栈帧(Stack Frame)中创建一个名为“锁记录”(Lock Record)的空间。

    2. JVM使用CAS操作,尝试将对象头的内容拷贝到这个锁记录中,并将对象头更新为一个指向该锁记录的指针。

    3. 如果CAS成功,该线程就获得了轻量级锁,锁标志位变为00

  • 解锁过程

    1. 线程使用CAS操作,尝试将之前保存在锁记录中的对象头内容再写回到对象头。

    2. 如果成功,表示没有其他线程竞争,解锁完成。

    3. 如果失败,说明在持有锁期间,有其他线程尝试获取该锁,导致锁已经膨胀成了重量级锁。此时,线程会进入重量级锁的解锁流程,去唤醒正在等待的线程。

3. 自旋与升级

如果线程获取轻量级锁的CAS操作失败,JVM不会立即将线程挂起。它会认为其他线程很快就会释放锁,因此会让当前线程进行“自旋”,即执行一个空循环,稍等片刻再尝试获取。这避免了线程进入内核态的阻塞操作。

  • 自适应自旋:JVM会根据上一次自旋的成功率和锁持有者的状态来决定自旋的次数,而不是固定次数。

  • 锁膨胀:如果自旋一定次数后(或者有其他线程也在自旋等待同一个锁),仍然没有获取到锁,说明竞争比较激烈。此时,轻量级锁就会膨胀(Inflate)为重量级锁

第四阶段:重量级锁 (Heavyweight Lock)

1. 升级时机

当多个线程激烈竞争同一个锁,自旋也无法解决问题时,锁就会升级为重量级锁。

2. 工作原理

  • 实现:重量级锁是Synchronized最原始的实现方式,它依赖于操作系统的互斥量(Mutex Lock)来实现。对象头中的锁标志位会变为10,并且指针会指向一个monitor对象。

  • 阻塞与唤醒:当一个线程尝试获取重量级锁失败后,它不会再自旋,而是会被直接放入该锁的等待队列中,并进入阻塞(Blocked)状态。此时,线程会从用户态切换到内核态,CPU被释放给其他线程。

  • 释放:当持有锁的线程释放锁时,它会唤醒等待队列中的一个或多个线程,被唤醒的线程会重新参与锁的竞争。这个过程涉及用户态和内核态之间的切换,开销是最大的。