← 返回 JUC 列表

Lock 体系

Lock 体系

一、基础认知

1.1 Lock 接口核心方法

Lock 是 JUC 包中锁的顶层接口,定义了以下核心方法:

java
public interface Lock {
    void lock();                // 获取锁(阻塞,直到获取成功)
    void lockInterruptibly();   // 可中断地获取锁(阻塞时可以被中断)
    boolean tryLock();          // 尝试获取锁(立即返回,成功 true / 失败 false)
    boolean tryLock(long time, TimeUnit unit); // 带超时的尝试获取
    void unlock();              // 释放锁
    Condition newCondition();   // 创建条件队列
}

使用铁律:lock()/unlock() 必须配对,unlock() 必须在 finally 中调用。

java
lock.lock();
try {
    // 临界区代码
} finally {
    lock.unlock(); // 保证锁一定能释放
}

1.2 Lock 与 synchronized 对比(面试必问)

对比维度 synchronized Lock(ReentrantLock)
关键字 vs API JVM 关键字 Java API 接口
锁获取/释放 JVM 自动释放 手动 lock/unlock,finally 中释放
可中断性 ❌ 不能响应中断 ✅ lockInterruptibly() 支持
超时获取 ❌ 不支持 ✅ tryLock(time, unit) 支持
公平性 ❌ 非公平 ✅ 支持公平/非公平构造
多个条件 ❌ 一个 wait 队列 ✅ 多个 Condition 精准唤醒
锁状态检测 ❌ 无法检测 ✅ isLocked() / isHeldByCurrentThread()
性能 JDK 6+ 优化后与 Lock 相当 同级别
编码复杂度 简单 较复杂(需手动释放)

选型建议:简单场景优先用 synchronized,需要超时/中断/多条件时用 Lock。


二、核心锁实现

2.1 ReentrantLock(可重入锁)

什么是可重入?

同一个线程可以多次获取同一把锁,不会产生死锁。每次 lock() 重入计数 +1,每次 unlock() 计数 -1,减到 0 才真正释放。

java
public void outer() {
    lock.lock();
    try {
        inner(); // 同一个线程可以再次获取锁
    } finally {
        lock.unlock();
    }
}

public void inner() {
    lock.lock();
    try {
        // 即使 outer 已持锁,这里也能获取成功(可重入)
    } finally {
        lock.unlock();
    }
}

公平模式 vs 非公平模式

java
// 非公平锁(默认):线程先抢锁,抢不到才排队
ReentrantLock unfairLock = new ReentrantLock(false);

// 公平锁:FIFO 排队,先来的先得
ReentrantLock fairLock = new ReentrantLock(true);
类型 优点 缺点 适用场景
非公平(默认) 吞吐量高,减少线程切换 可能线程饥饿 通用场景,吞吐量优先
公平 等待时间公平,无饥饿 吞吐量低(约 1/3) 要求公平调度、持有锁时间长的场景

底层区别(基于 AQS):

  • 非公平锁:lock() 时先 CAS 抢一次锁,抢不到才走 acquire 流程
  • 公平锁:tryAcquire() 先检查 hasQueuedPredecessors(),CLH 队列中有前驱则排队

2.2 ReentrantReadWriteLock(读写分离锁)

读写规则

读锁(共享锁)   +   读锁(共享锁)   =   ✅ 允许多线程并发读
读锁(共享锁)   +   写锁(独占锁)   =   ❌ 读写互斥
写锁(独占锁)   +   写锁(独占锁)   =   ❌ 写写互斥

核心应用:缓存

java
public class CacheService {
    private final ReentrantReadWriteLock rwLock = new ReentrantReadWriteLock();
    private final Lock readLock = rwLock.readLock();
    private final Lock writeLock = rwLock.writeLock();
    private final Map<String, Object> cache = new HashMap<>();

    // 读 — 多个线程并发读
    public Object get(String key) {
        readLock.lock();
        try {
            return cache.get(key);
        } finally {
            readLock.unlock();
        }
    }

    // 写 — 独占
    public void put(String key, Object value) {
        writeLock.lock();
        try {
            cache.put(key, value);
        } finally {
            writeLock.unlock();
        }
    }

    // 锁降级:写 → 读(保证写完后能继续读)
    public void updateAndRead(String key, Object value) {
        writeLock.lock();
        try {
            cache.put(key, value);
            readLock.lock(); // 写锁持有中,获取读锁成功(锁降级)
        } finally {
            writeLock.unlock(); // 释放写锁,降级为读锁状态
        }
        try {
            Object val = cache.get(key); // 此时仍持有读锁,数据安全
        } finally {
            readLock.unlock();
        }
    }
}

注意事项

  • 锁降级(写→读):✅ 允许,写线程可以降级为读线程,保证数据可见性
  • 锁升级(读→写):❌ 不允许,多个读线程同时尝试升级写锁会导致死锁
  • 写锁饥饿:大量读线程持续持有读锁时,写线程可能长时间无法获取写锁
  • 适用场景:读多写少(缓存、配置中心、路由表)

2.3 StampedLock(JDK 8 邮戳锁)

为什么要 StampedLock?

ReentrantReadWriteLock 存在两个问题:

  1. 写饥饿:读线程多时写线程难以获取写锁
  2. 悲观读:读的时候强制加锁,影响了读性能

StampedLock 通过乐观读解决这两个问题。

三种锁模式

模式 方法 行为 性能
写锁 writeLock() 独占,全部互斥
悲观读锁 readLock() 共享锁,和写锁互斥
乐观读 tryOptimisticRead() 不加锁,仅返回 stamp 戳,需 validate() 验证 最高

乐观读核心用法

java
public class PointService {
    private final StampedLock stampedLock = new StampedLock();
    private int x = 0, y = 0;

    // 写操作 — 独占
    public void move(int dx, int dy) {
        long stamp = stampedLock.writeLock();
        try {
            x += dx;
            y += dy;
        } finally {
            stampedLock.unlockWrite(stamp);
        }
    }

    // 乐观读 — 不加锁,性能极高
    public int readOptimistic() {
        // ① 乐观读:拿到 stamp 戳
        long stamp = stampedLock.tryOptimisticRead();
        int curX = x, curY = y;

        // ② 验证:数据有没有被写线程修改过?
        if (!stampedLock.validate(stamp)) {
            // ③ 被修改了,升级为悲观读锁
            stamp = stampedLock.readLock();
            try {
                curX = x;
                curY = y;
            } finally {
                stampedLock.unlockRead(stamp);
            }
        }
        return curX + curY;
    }
}

乐观读的原理: 读取时不加任何锁(不阻塞任何线程),读完验证 stamp 戳有无变化。无变化则读取有效;有变化则退化为悲观读。

注意事项

特性 说明
不可重入 同一个线程不能多次获取同一把锁,重入会导致死锁
不支持 Condition newCondition() 抛 UnsupportedOperationException
乐观读不是锁 必须配合 validate() 验证,否则可能读到脏数据
中断可能 CPU 飙升 内部使用自旋锁实现,中断操作可能造成自旋
适用场景 读远多于写、超高并发、对数据一致性要求不极端

三、等待唤醒机制(Condition)

3.1 Condition 是什么?

Condition 将 等待/唤醒 绑定,实现精准的线程间通信。

对比 Object wait/notify Condition await/signal
绑定关系 绑定 synchronized 绑定 Lock
条件队列数量 一个对象一个 wait 队列 一个 Lock 可以创建多个 Condition
唤醒粒度 notify() 随机唤醒一个 signal() 精准唤醒特定条件的线程
中断响应 抛 InterruptedException 抛 InterruptedException

3.2 多条件精准唤醒(经典:生产者-消费者)

java
public class BoundedQueue<T> {
    private final ReentrantLock lock = new ReentrantLock();
    private final Condition notFull  = lock.newCondition();  // 不满条件
    private final Condition notEmpty = lock.newCondition();  // 不空条件
    private final Queue<T> queue = new LinkedList<>();
    private final int capacity;

    public BoundedQueue(int capacity) {
        this.capacity = capacity;
    }

    // 生产者:队列满则等待「不满」信号
    public void put(T item) throws InterruptedException {
        lock.lock();
        try {
            while (queue.size() == capacity) {
                notFull.await();    // 队列满了,等待不满条件
            }
            queue.offer(item);
            notEmpty.signal();      // 通知消费者:队列不空了
        } finally {
            lock.unlock();
        }
    }

    // 消费者:队列空则等待「不空」信号
    public T take() throws InterruptedException {
        lock.lock();
        try {
            while (queue.isEmpty()) {
                notEmpty.await();   // 队列空了,等待不空条件
            }
            T item = queue.poll();
            notFull.signal();       // 通知生产者:队列不满了
            return item;
        } finally {
            lock.unlock();
        }
    }
}

优势:notFullnotEmpty 两个条件分开等待,不会出现用 synchronizednotifyAll() 唤醒了不该唤醒的线程的问题。

3.3 await/signal 使用规范

java
// await() 必须在循环中检查条件(防止假唤醒)
while (条件不满足) {
    condition.await(); // 不是 if,是 while!
}

// signal() 必须在条件满足时调用
if (条件满足) {
    condition.signal(); // 或 signalAll()
}
方法 行为
await() 释放锁,进入条件队列等待
await(time, unit) 超时等待
signal() 唤醒条件队列中的一个线程(移入同步队列竞争锁)
signalAll() 唤醒条件队列中所有线程

四、底层基础

4.1 LockSupport(线程阻塞/唤醒工具)

核心方法

java
LockSupport.park();            // 阻塞当前线程
LockSupport.parkNanos(long nanos);   // 超时阻塞
LockSupport.unpark(Thread t);  // 唤醒指定线程

permit 许可机制

每个线程有一个「许可」(permit),值只能是 0 或 1:
  unpark(thread)  → 设置 permit = 1
  park()          → 如果 permit > 0,消耗 permit 并返回
                    如果 permit = 0,阻塞直到 permit 变为 1

这也是 unpark 可以先于 park 调用 的原因——提前发放 permit,后面的 park() 不会阻塞。

与 wait/notify 对比

对比点 Object.wait/notify LockSupport.park/unpark
是否需要先持有锁 ✅ 必须在 synchronized 内 ❌ 完全不需要
唤醒方式 notify() 随机唤醒一个 unpark(thread) 精准唤醒指定线程
调换顺序 ❌ notify 先于 wait → wait 永远等不到 ✅ unpark 先于 park → park 直接返回
中断行为 抛 InterruptedException 不抛异常,直接返回需自行检查中断标志
应用 所有 Object 对象 AQS 的基石

示例

java
// 精准唤醒(对比 notify 的随机唤醒优势明显)
Thread worker = new Thread(() -> {
    System.out.println("等待任务...");
    LockSupport.park();
    System.out.println("被唤醒了!");
});
worker.start();

Thread.sleep(1000);
LockSupport.unpark(worker); // 精准唤醒 worker 线程

4.2 AQS 绑定逻辑

Lock 体系所有核心类都依赖 AQS 实现:

ReentrantLock          → 内部类 Sync extends AbstractQueuedSynchronizer
ReentrantReadWriteLock → 内部类 Sync extends AbstractQueuedSynchronizer
StampedLock            → 自实现 CLH(但思路类似 AQS)
LockSupport            → AQS 底层阻塞/唤醒的实现工具
Condition              → AQS 内部 ConditionObject 实现

学习路径建议: 先理解 AQS(见 3_aqs.md),再学 Lock 实现会豁然开朗。


五、特性要点

5.1 五大特性总览

特性 说明 涉及类
可重入 同一线程可多次获取同一把锁 ReentrantLock、ReadWriteLock
可中断 等待锁时可以被其他线程中断 ReentrantLock.lockInterruptibly()
可超时 指定时间内获取不到就放弃 ReentrantLock.tryLock(time, unit)
公平性 支持公平/非公平两种模式 ReentrantLock(boolean fair)
多条件 一个 Lock 可创建多个 Condition ReentrantLock.newCondition()

5.2 锁降级 vs 锁升级

概念 定义 ReentrantReadWriteLock StampedLock
锁降级 写锁 → 读锁 ✅ 支持 ✅ 支持 tryConvertToReadLock()
锁升级 读锁 → 写锁 死锁风险(多个读线程都要写) ✅ 支持 tryConvertToWriteLock()

锁降级的作用: 写完数据后不释放锁就获取读锁,保证写完之后到真正释放读锁这段时间,数据不会被其他线程修改。

5.3 锁饥饿问题

锁类型 饥饿风险 原因 解决
ReentrantLock(非公平) 新线程插队 用公平锁
ReentrantReadWriteLock 写锁饥饿 大量读线程 用 StampedLock
StampedLock 乐观读不阻塞写 乐观读本质无锁

六、实战与面试重点

6.1 锁的标准写法(try-finally)

java
// 模式一:lock + try-finally(最常用)
lock.lock();
try {
    // 业务逻辑
} finally {
    lock.unlock();
}

// 模式二:tryLock + 双检查
if (lock.tryLock()) {
    try {
        // 业务逻辑
    } finally {
        lock.unlock();
    }
} else {
    // 获取失败的处理
}

// 模式三:lockInterruptibly
lock.lockInterruptibly();
try {
    // 业务逻辑(可被中断取消)
} finally {
    lock.unlock();
}

6.2 各类锁业务选型

业务场景 推荐锁 原因
简单临界区、代码块 synchronized 简洁、自动释放、不易出错
需要超时等待 ReentrantLock.tryLock() 避免死锁
可中断等待 ReentrantLock.lockInterruptibly() 支持取消
读多写少缓存 ReentrantReadWriteLock 读读并发
超高并发读场景 StampedLock 乐观读 读完全不加锁
需要多个条件队列 ReentrantLock + Condition 精准唤醒
简单等待通知 synchronized + wait/notify 够用

6.3 公平/非公平锁底层区别

java
// === 非公平锁 ===
final void lock() {
    // 一进来就 CAS 抢一次,不管队列里有没有人在等
    if (compareAndSetState(0, 1))
        setExclusiveOwnerThread(Thread.currentThread());
    else
        acquire(1); // 抢不到才走排队流程
}

// === 公平锁 ===
protected final boolean tryAcquire(int acquires) {
    // 先检查队列中是否有前驱在等待
    if (!hasQueuedPredecessors() &&     // 唯一区别!
        compareAndSetState(0, acquires)) {
        setExclusiveOwnerThread(current);
        return true;
    }
}

非公平锁为什么吞吐量更高?

  • 非公平锁:线程切换次数少(刚释放锁的线程有机会立即再次获取),缓存不失效
  • 公平锁:线程切换频繁,上下文切换开销大

6.4 读写锁 vs 邮戳锁场景

对比 ReentrantReadWriteLock StampedLock
读读并发
读写互斥 ✅(悲观读)/ ❌(乐观读)
写饥饿 ❌ 可能发生 ✅ 乐观读不阻塞写
性能 (乐观读无锁)
可重入
Condition
适用 缓存、配置 超高并发读、数据一致性要求不极端

七、常见面试题

Q1:synchronized 和 ReentrantLock 的区别?

见第一章对比表。核心:synchronized 是关键字自动释放,Lock 是 API 手动释放但更灵活(可中断、可超时、多条件)。

Q2:Condition 的 await/signal 和 Object wait/notify 的区别?

维度 wait/notify await/signal
绑定锁 synchronized Lock
条件队列数 单个 多个(精准唤醒)
唤醒 随机或全部 精准唤醒特定条件
使用 简单 灵活

Q3:LockSupport.park/unpark 的原理?

基于 permit 许可机制(类似只有 0 或 1 的信号量)。unpark 设置 permit = 1,park 消耗 permit,permmit = 0 则阻塞。无需先持有锁,可以精准唤醒指定线程。

Q4:如何避免死锁?

  1. 锁排序:所有线程按固定顺序获取锁
  2. tryLock 超时:获取不到主动放弃
  3. 缩小锁范围:只在需要时持有锁
  4. 避免锁嵌套:减少一个方法中持有多个锁

Q5:锁降级和锁升级分别指什么?

  • 锁降级(写锁→读锁):写线程获取读锁后释放写锁,保证数据可见性
  • 锁升级(读锁→写锁):ReadWriteLock 不支持(死锁风险),StampedLock 支持

Q6:StampedLock 的乐观读为什么比 ReadWriteLock 快?

乐观读不加任何锁,不阻塞任何线程(包括写线程),仅通过验证 stamp 戳判断数据是否有效。消除了读与读、读与写之间的竞争。

Q7:公平锁和非公平锁怎么选?

默认用非公平锁(吞吐量高)。只有需要保证等待时间公平、避免线程饥饿时才用公平锁。

Q8:ReentrantLock 和 StampedLock 的适用场景?

  • ReentrantLock:通用互斥场景,需要可重入、Condition
  • StampedLock:读远多于写的超高并发场景,能接受不可重入和无 Condition