Skip to content

约 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 方法,使得比较+写入是一个原子操作

  1. 获取目标值
  2. 调用compareAndSet(oldValue, oldValue + 1)
  3. 若执行修改方法前没有其他线程修改,则更新
  4. 若执行修改方法前值已经被其他线程修改,则本次不操作,回到阶段 1

CLH 队列

原始 CLH 队列结构 image 参加并发竞争的线程将被加入一个单向队列中,每个线程作为其中的一个节点,每个节点通过 [[#CAS]] 的方式检查自己的前驱节点的状态 CLH 队列相较于所有线程 CAS 检查同一个对象,每个节点只检查自己的前驱节点(一哄而上变成排队),避免了饥饿现象

AQS

AbstractQueuedSynchronizer 抽象队列同步器 AQS 中 CLH 的优化版本 image 在原始的 CLH 实现中,每个节点都在不断的 CAS 检查自己的前驱节点,有可能造成大量的 CPU 资源浪费 AQS 的实现中,将队列的数据结构更改为双向链表,如此便可使队列中的线程进入阻塞状态,当前驱节点释放锁后由其唤醒当前节点即可,避免了不必要的 CPU 运算 维护一个private volatile int state;表示同步状态,并管理一个CLH队列

  1. 检查对象是否被占用,若未被占用则直接调用
  2. 若对象被占用,则当前线程加入 CLH 队列中,进入阻塞状态
  3. 当锁持有线程释放后唤醒后驱节点中的线程

注意: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 为了在性能与安全性间取得平衡,不会直接使用重量级锁,而是根据竞争情况自动升级。该过程通常是不可逆的。

  1. 偏向锁 (Biased Locking)
    • 场景:锁总是由同一线程获取。
    • 实现:在对象头记录线程 ID。该线程再次进入时只需对比 ID,无需 CAS 操作。
  2. 轻量级锁 (Lightweight Locking)
    • 场景:存在少量竞争,但同步块执行极快。
    • 实现:线程通过 CAS 尝试获取锁。失败后不会立即阻塞,而是执行自旋(原地打转),尝试等待占用锁的线程快速释放。
  3. 重量级锁 (Heavyweight Locking)
    • 场景:竞争激烈,自旋多次仍未获取锁。
    • 实现:升级为管程(Monitor)
    • 代价:涉及内核态(Kernel Mode)切换。操作系统会挂起当前线程(上下文切换),这比用户态的 CAS 要消耗多出数千倍的 CPU 周期。

为什么通常不降级? 为了避免在激烈竞争环境下,锁在不同状态间频繁震荡导致的额外性能开销。JVM 假设一旦发生过激烈竞争,后续大概率仍会竞争。


对象监视器 (Monitor)

对象监视器内部维护两个队列:

  • Wait Set(等待集):存放调用了 wait() 的线程。
  • Entry Set(入口集/锁竞争队列):存放等待获取锁而被阻塞的线程。

Object 中存在三个方法:notify()notifyAll()wait(),前两者将等待集中的线程移动至入口集中,而 wait() 则是将当前线程释放锁并放入等待集中。 当对象的锁被释放后,JVM 将唤醒所有处于入口集中的线程,使之参与锁的竞争。

#review