跳转到内容

Lock 体系

synchronized 能解决互斥问题,但有三个硬伤:

  • 抢不到锁只能死等,无法中断、无法超时;
  • 只有一个等待队列,notifyAll() 唤醒所有线程,浪费 CPU。
  • 无法实现读读共享;

Lock 体系在 synchronized 基础上提供更细粒度的控制。

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 {
// 超时处理
}
公平锁非公平锁(默认)
抢锁方式先检查队列,有人排队则入队上来直接 CAS 抢,失败再入队
吞吐量较低较高
饥饿风险无有(队列中线程可能长期抢不到)
适用场景严格公平性要求绝大多数场景

同一线程可以多次获取同一把锁,每次 lock() 计数器 +1,每次 unlock() 计数器 -1,归零才真正释放。底层通过 AQS 的 state 字段记录重入次数。

lock.lock(); // state = 1
lock.lock(); // state = 2(同一线程,不阻塞)
lock.unlock(); // state = 1
lock.unlock(); // state = 0,真正释放

Object.notifyAll() 唤醒所有等待线程(生产者 + 消费者都唤醒),再竞争,浪费 CPU。

一把锁可以有多个 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();
}
ConditionObject.wait/notify
等待方法await()wait()
唤醒方法signal() / signalAll()notify() / notifyAll()
条件队列数量一把锁可多个只有一个
精确唤醒支持不支持
使用前提持有 Lock持有 synchronized 锁

读-读不互斥,读-写互斥,写-写互斥。 适合读多写少场景(缓存、配置中心)。

已有读锁已有写锁无锁
请求读锁兼容(共享)阻塞获取
请求写锁阻塞阻塞获取
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();
}

持有写锁时可以再获取读锁,释放写锁后降级为读锁。反向不支持(读锁不能升级为写锁)。

writeLock.lock();
try {
data = compute();
readLock.lock(); // 持有写锁时先拿读锁
} finally {
writeLock.unlock(); // 释放写锁,完成降级
}
try {
use(data); // 继续用读锁保护
} finally {
readLock.unlock();
}

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); }
场景选择
简单临界区,代码量少synchronized,简洁,JVM 自动释放
需要超时 / 可中断 / 非阻塞尝试ReentrantLock
需要多条件精确唤醒ReentrantLock + Condition
读多写少,并发读性能要求高ReentrantReadWriteLock
读极多、写极少,追求极致性能StampedLock 乐观读

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。