锁
约 1632 字大约 5 分钟
2026-02-26
可重入锁 基本用法
import java.util.concurrent.locks.ReentrantLock;
public class Counter {
private int count = 0;
private final ReentrantLock lock = new ReentrantLock();
public void increment() {
lock.lock(); // 获取锁
try {
count++;
} finally {
lock.unlock(); // 必须在 finally 中释放锁
}
}
}所谓可重入,即同一线程下可以多次获取同一把锁。在以下代码中,两个方法中需要获取的是同一把锁,能够保证同一线程下不会出现「我等我自己」的情况
private ReentantLock lock = new ReentrantLock();
public void methodA() {
lock.lock();
try {
System.out.println("methodA");
methodB(); // 同一线程再次获取锁
} finally {
lock.unlock();
}
}
public void methodB() {
lock.lock(); // 不会阻塞,因为是同一个线程
try {
System.out.println("methodB");
} finally {
lock.unlock();
}
}实现
ReentrantLock底层基于 AQS(AbstractQueuedSynchronizer)实现,即 CAS + CLH 队列 ReentrantLock的所有权归属与上一个成功lock且未释放锁的线程
- 若锁没有被任何线程所拥有,则当任意线程调用时将成功地获取到锁
- 若当前线程拥有该锁,则当当前线程调用该锁时将会立即返回成功。(可重入)
- 若当前锁被其他线程拥有,当前线程调用后将会被加入 AQS 等待队列中并进入阻塞状态等待释放
通过构造函数ReentrantLock(boolean fair)可以开启「公平锁」的模式,该模式下将优先使等待时间最长的线程获取锁
- 非公平锁模式:新调用
acquire()的线程将执行有限次数的 CAS 操作尝试获取锁对象,若获取失败才加入队列。该模式下可能会导致处于队列中的线程处于「饥饿」状态 - 公平锁模式:任何新调用
acquire()的线程,若锁对象被占用,则直接加入队列,不与队列中的线程直接竞争
CAS
如果当前值等于期望值(expected),就把它更新为新值(new),否则不做操作
通过调用 native 方法,使得比较+写入是一个原子操作
- 获取目标值
- 调用
compareAndSet(oldValue, oldValue + 1) - 若执行修改方法前没有其他线程修改,则更新
- 若执行修改方法前值已经被其他线程修改,则本次不操作,回到阶段 1
CLH 队列
原始 CLH 队列结构
参加并发竞争的线程将被加入一个单向队列中,每个线程作为其中的一个节点,每个节点通过 [[#CAS]] 的方式检查自己的前驱节点的状态 CLH 队列相较于所有线程 CAS 检查同一个对象,每个节点只检查自己的前驱节点(一哄而上变成排队),避免了饥饿现象
AQS
AbstractQueuedSynchronizer 抽象队列同步器 AQS 中 CLH 的优化版本
在原始的 CLH 实现中,每个节点都在不断的 CAS 检查自己的前驱节点,有可能造成大量的 CPU 资源浪费 AQS 的实现中,将队列的数据结构更改为双向链表,如此便可使队列中的线程进入阻塞状态,当前驱节点释放锁后由其唤醒当前节点即可,避免了不必要的 CPU 运算 维护一个private volatile int state;表示同步状态,并管理一个CLH队列
- 检查对象是否被占用,若未被占用则直接调用
- 若对象被占用,则当前线程加入 CLH 队列中,进入阻塞状态
- 当锁持有线程释放后唤醒后驱节点中的线程
注意:AQS 的 state 变量使用 volatile 修饰,结合 CAS 操作,它在底层同样触发内存屏障,保证了获取锁/释放锁时的可见性语义与 synchronized 完全一致。
synchronized
synchronized 修饰方法或代码块,其通过与对象关联的内置锁或者称为监视器锁(Monitor Lock)实现。
实现原理
synchronized 的安全性建立在硬件级别的原子指令和内存模型之上:
对象头 (Mark Word)
锁的状态保存在 Java 对象的对象头(Object Header)中的 Mark Word 字段里。
- 动态复用:为了节省空间,
Mark Word会根据锁的状态(无锁、偏向、轻量、重量)复用 64 位空间,记录线程 ID、指向栈中锁记录的指针、或指向 Monitor 的指针。
内存屏障 (Memory Barrier)
为了解决 CPU 缓存导致的状态更新不及时问题,JVM 在 synchronized 的边界插入内存屏障:
- 进入 (Lock):强制使当前线程的本地缓存失效,必须从主内存读取最新数据。
- 退出 (Unlock):强制将同步块内修改的数据和锁标记刷回主内存,确保后续线程可见。
锁升级机制 (Lock Escalation)
JVM 为了在性能与安全性间取得平衡,不会直接使用重量级锁,而是根据竞争情况自动升级。该过程通常是不可逆的。
- 偏向锁 (Biased Locking)
- 场景:锁总是由同一线程获取。
- 实现:在对象头记录线程 ID。该线程再次进入时只需对比 ID,无需 CAS 操作。
- 轻量级锁 (Lightweight Locking)
- 场景:存在少量竞争,但同步块执行极快。
- 实现:线程通过 CAS 尝试获取锁。失败后不会立即阻塞,而是执行自旋(原地打转),尝试等待占用锁的线程快速释放。
- 重量级锁 (Heavyweight Locking)
- 场景:竞争激烈,自旋多次仍未获取锁。
- 实现:升级为管程(Monitor)。
- 代价:涉及内核态(Kernel Mode)切换。操作系统会挂起当前线程(上下文切换),这比用户态的 CAS 要消耗多出数千倍的 CPU 周期。
为什么通常不降级? 为了避免在激烈竞争环境下,锁在不同状态间频繁震荡导致的额外性能开销。JVM 假设一旦发生过激烈竞争,后续大概率仍会竞争。
对象监视器 (Monitor)
对象监视器内部维护两个队列:
- Wait Set(等待集):存放调用了
wait()的线程。 - Entry Set(入口集/锁竞争队列):存放等待获取锁而被阻塞的线程。
Object 中存在三个方法:notify()、notifyAll()、wait(),前两者将等待集中的线程移动至入口集中,而 wait() 则是将当前线程释放锁并放入等待集中。 当对象的锁被释放后,JVM 将唤醒所有处于入口集中的线程,使之参与锁的竞争。
#review
