Lock 体系
1. 为什么需要 Lock?
Section titled “1. 为什么需要 Lock?”synchronized 能解决互斥问题,但有三个硬伤:
- 抢不到锁只能死等,无法中断、无法超时;
- 只有一个等待队列,
notifyAll()唤醒所有线程,浪费 CPU。 - 无法实现读读共享;
Lock 体系在 synchronized 基础上提供更细粒度的控制。
2. 整体结构
Section titled “2. 整体结构”3. ReentrantLock
Section titled “3. ReentrantLock”3.1. 基本用法
Section titled “3.1. 基本用法”ReentrantLock lock = new ReentrantLock(); // 默认非公平// ReentrantLock lock = new ReentrantLock(true); // 公平锁
lock.lock();try { // 临界区} finally { lock.unlock(); // 必须在 finally,否则异常时锁永远不释放}3.2. 四个 synchronized 做不到的能力
Section titled “3.2. 四个 synchronized 做不到的能力”| 能力 | API | 说明 |
|---|---|---|
| 可中断等锁 | lockInterruptibly() | 等待中可响应 interrupt(),避免死等 |
| 超时尝试 | tryLock(500, MILLISECONDS) | 等不到就放弃,返回 false |
| 非阻塞尝试 | tryLock() | 立刻返回,抢到 true,没抢到 false |
| 多条件队列 | newCondition() | 可精确唤醒不同类型的等待线程 |
// 超时尝试,避免死锁if (lock.tryLock(500, TimeUnit.MILLISECONDS)) { try { /* 临界区 */ } finally { lock.unlock(); }} else { // 超时处理}3.3. 公平锁 vs 非公平锁
Section titled “3.3. 公平锁 vs 非公平锁”| 公平锁 | 非公平锁(默认) | |
|---|---|---|
| 抢锁方式 | 先检查队列,有人排队则入队 | 上来直接 CAS 抢,失败再入队 |
| 吞吐量 | 较低 | 较高 |
| 饥饿风险 | 无 | 有(队列中线程可能长期抢不到) |
| 适用场景 | 严格公平性要求 | 绝大多数场景 |
3.4. 可重入原理
Section titled “3.4. 可重入原理”同一线程可以多次获取同一把锁,每次 lock() 计数器 +1,每次 unlock() 计数器 -1,归零才真正释放。底层通过 AQS 的 state 字段记录重入次数。
lock.lock(); // state = 1lock.lock(); // state = 2(同一线程,不阻塞)lock.unlock(); // state = 1lock.unlock(); // state = 0,真正释放4. Condition — 精确唤醒
Section titled “4. Condition — 精确唤醒”4.1. wait/notify 的问题
Section titled “4.1. wait/notify 的问题”Object.notifyAll() 唤醒所有等待线程(生产者 + 消费者都唤醒),再竞争,浪费 CPU。
4.2. Condition 解决方案
Section titled “4.2. Condition 解决方案”一把锁可以有多个 Condition,每个 Condition 是独立的等待队列,实现精确唤醒。
ReentrantLock lock = new ReentrantLock();Condition notFull = lock.newCondition(); // 队列未满Condition notEmpty = lock.newCondition(); // 队列非空
// 生产者lock.lock();try { while (queue.isFull()) { notFull.await(); // 满了,等"未满"条件 }
queue.add(item); notEmpty.signal(); // 只唤醒消费者} finally { lock.unlock();}
// 消费者lock.lock();try { while (queue.isEmpty()) { notEmpty.await(); // 空了,等"非空"条件 }
Item item = queue.poll(); notFull.signal(); // 只唤醒生产者} finally { lock.unlock();}4.3. Condition 对比 Object wait/notify
Section titled “4.3. Condition 对比 Object wait/notify”Condition | Object.wait/notify | |
|---|---|---|
| 等待方法 | await() | wait() |
| 唤醒方法 | signal() / signalAll() | notify() / notifyAll() |
| 条件队列数量 | 一把锁可多个 | 只有一个 |
| 精确唤醒 | 支持 | 不支持 |
| 使用前提 | 持有 Lock | 持有 synchronized 锁 |
5. ReentrantReadWriteLock
Section titled “5. ReentrantReadWriteLock”5.1. 核心思想
Section titled “5.1. 核心思想”读-读不互斥,读-写互斥,写-写互斥。 适合读多写少场景(缓存、配置中心)。
5.2. 互斥矩阵
Section titled “5.2. 互斥矩阵”| 已有读锁 | 已有写锁 | 无锁 | |
|---|---|---|---|
| 请求读锁 | 兼容(共享) | 阻塞 | 获取 |
| 请求写锁 | 阻塞 | 阻塞 | 获取 |
5.3. 基本用法
Section titled “5.3. 基本用法”ReadWriteLock rwLock = new ReentrantReadWriteLock();Lock readLock = rwLock.readLock();Lock writeLock = rwLock.writeLock();
// 读操作 — 多线程并发readLock.lock();try { return cache.get(key);} finally { readLock.unlock();}
// 写操作 — 独占writeLock.lock();try { cache.put(key, value);} finally { writeLock.unlock();}5.4. 锁降级(写锁 → 读锁)
Section titled “5.4. 锁降级(写锁 → 读锁)”持有写锁时可以再获取读锁,释放写锁后降级为读锁。反向不支持(读锁不能升级为写锁)。
writeLock.lock();try { data = compute(); readLock.lock(); // 持有写锁时先拿读锁} finally { writeLock.unlock(); // 释放写锁,完成降级}try { use(data); // 继续用读锁保护} finally { readLock.unlock();}6. StampedLock(了解)
Section titled “6. StampedLock(了解)”JDK 8 引入,比 ReentrantReadWriteLock 性能更高,增加了乐观读模式。
StampedLock sl = new StampedLock();
// 乐观读(无锁,读完校验)long stamp = sl.tryOptimisticRead();int val = data; // 读数据if (!sl.validate(stamp)) { // 校验期间有写入 stamp = sl.readLock(); // 降级为悲观读锁 try { val = data; } finally { sl.unlockRead(stamp); }}
// 写锁long stamp = sl.writeLock();try { data = newVal; }finally { sl.unlockWrite(stamp); }7. 选型指南
Section titled “7. 选型指南”| 场景 | 选择 |
|---|---|
| 简单临界区,代码量少 | synchronized,简洁,JVM 自动释放 |
| 需要超时 / 可中断 / 非阻塞尝试 | ReentrantLock |
| 需要多条件精确唤醒 | ReentrantLock + Condition |
| 读多写少,并发读性能要求高 | ReentrantReadWriteLock |
| 读极多、写极少,追求极致性能 | StampedLock 乐观读 |
8. 面试高频问题
Section titled “8. 面试高频问题”Q:ReentrantLock 和 synchronized 的区别?
A:ReentrantLock 支持超时尝试、可中断、非阻塞 tryLock、多条件队列;synchronized 是关键字,JVM 层面保证,代码更简洁,异常自动释放锁。性能上 JDK 6 之后差距不大,优先用 synchronized,有特殊需求再用 ReentrantLock。
Q:公平锁为什么吞吐量低?
A:公平锁每次获取锁都要检查 CLH 队列,即使锁空闲也要判断有无线程在排队。而非公平锁上来直接 CAS,有概率直接拿到,避免了线程挂起和唤醒的开销(线程切换代价远大于一次 CAS)。
Q:读写锁为什么不支持锁升级(读→写)?
A:死锁风险。若线程 A 和线程 B 都持有读锁,同时尝试升级为写锁,都在等对方释放读锁,形成死锁。JDK 设计时直接禁止了锁升级。
Q:Condition 的 await() 和 Object 的 wait() 有什么相同点?
A:① 都必须在持有锁的情况下调用;② 调用后都会释放锁并进入等待;③ 被唤醒后都需要重新竞争锁;④ 都可能发生虚假唤醒(spurious wakeup),所以等待条件要用 while 而不是 if。