1 synchronized 边界
在使用 synchronized 的过程中,开发者往往会逐渐遇到几个无法绕开的问题。
第一,线程在等待锁的过程中无法被中断。一旦线程进入 BLOCKED 状态,除非获得锁,否则无法响应中断信号,这在需要快速取消任务的系统中是不可接受的。
第二,无法限制等待时间。synchronized 要么获取成功,要么无限期等待,这使得线程在高竞争环境下容易堆积,最终拖垮系统。
第三,条件同步能力不足。所有 wait 线程共享同一个隐式条件队列,notify 无法精确控制唤醒对象,复杂条件下代码迅速失控。
Lock 体系并不是为了“替代” synchronized,而是为了突破这些设计边界。
2 Lock 接口的抽象含义
2.1 Lock 是一种并发协议
Lock 的本质并不是一把锁,而是一套线程协作协议。
它明确规定了三件事情:
线程如何尝试进入临界区
失败时线程处于什么状态
线程如何有序地离开临界区
这一点决定了 Lock 必须由代码显式控制,而不能由编译器隐式插入。
2.2 Lock 的基本使用模式
所有 Lock 实现都遵循同一套使用模式:
Lock lock = new ReentrantLock();
lock.lock();
try {
// 临界区
} finally {
lock.unlock();
}
这段结构不是习惯问题,而是语义问题。
unlock 放在 finally 中,意味着无论临界区发生什么异常,锁都必须被释放,否则后续线程将永久阻塞。
2.3 显式锁带来的风险
显式锁的灵活性是以风险为代价的,最常见的问题包括:
忘记 unlock
重复 unlock
在未持有锁的情况下调用 unlock
锁粒度过大导致性能退化
因此,Lock 更适合经验较多、对并发行为有明确预期的场景。
3 ReentrantLock 的深入分析
3.1 可重入锁的真实价值
可重入锁解决的不是“能不能加锁”的问题,而是“调用路径是否安全”的问题。
class Service {
private final Lock lock = new ReentrantLock();
public void methodA() {
lock.lock();
try {
methodB();
} finally {
lock.unlock();
}
}
public void methodB() {
lock.lock();
try {
// 操作共享资源
} finally {
lock.unlock();
}
}
}
如果锁不可重入,methodA 在调用 methodB 时将直接死锁。
ReentrantLock 内部通过线程标识和重入计数,避免了这一问题。
3.2 公平锁与非公平锁的运行差异
Lock fairLock = new ReentrantLock(true);
Lock unfairLock = new ReentrantLock(false);
非公平锁在 lock 时,会先尝试一次 CAS 抢占锁,即使队列中已有等待线程。
这种“插队”行为减少了线程切换次数,在高并发下具有明显的性能优势。
公平锁则严格按照队列顺序获取锁,线程调度更加可预测,但吞吐量更低。
在绝大多数业务系统中,非公平锁是更合理的默认选择。
3.3 tryLock 的工程意义
if (lock.tryLock()) {
try {
// 获取成功
} finally {
lock.unlock();
}
} else {
// 获取失败,走降级逻辑
}
tryLock 使并发控制从“阻塞模型”转变为“分支模型”。
常见应用包括:
避免线程堆积
实现快速失败
实现多资源尝试锁定
3.4 可中断锁的使用场景
try {
lock.lockInterruptibly();
try {
// 临界区
} finally {
lock.unlock();
}
} catch (InterruptedException e) {
// 响应中断
}
lockInterruptibly 的意义在于:
线程在等待锁的过程中可以被中断,而不是只能被动等待。
这在以下场景中非常关键:
线程池任务取消
服务快速停机
超时控制链路
4 Condition 的完整条件同步模型
4.1 wait/notify 的结构性问题
在 synchronized 模型下:
synchronized (obj) {
while (!condition) {
obj.wait();
}
}
所有 wait 的线程都进入同一个等待集合,notify 唤醒的是随机线程。
当条件复杂时,大量线程被错误唤醒,又立即进入等待,造成性能浪费。
4.2 使用 Condition 拆分业务条件
Lock lock = new ReentrantLock();
Condition notEmpty = lock.newCondition();
Condition notFull = lock.newCondition();
每个 Condition 表示一种业务状态,等待线程只会被相关信号唤醒。
4.3 生产者消费者完整示例
class BoundedQueue {
private final Object[] items = new Object[10];
private int count, putIndex, takeIndex;
private final Lock lock = new ReentrantLock();
private final Condition notEmpty = lock.newCondition();
private final Condition notFull = lock.newCondition();
public void put(Object x) throws InterruptedException {
lock.lock();
try {
while (count == items.length) {
notFull.await();
}
items[putIndex] = x;
putIndex = (putIndex + 1) % items.length;
count++;
notEmpty.signal();
} finally {
lock.unlock();
}
}
public Object take() throws InterruptedException {
lock.lock();
try {
while (count == 0) {
notEmpty.await();
}
Object x = items[takeIndex];
takeIndex = (takeIndex + 1) % items.length;
count--;
notFull.signal();
return x;
} finally {
lock.unlock();
}
}
}
这里的关键点不在代码本身,而在于:
条件队列按业务语义拆分
signal 精确唤醒需要继续执行的线程
避免无效唤醒和无意义竞争
5 ReadWriteLock 的并发控制细节
5.1 读写锁的并发模型
ReadWriteLock rwLock = new ReentrantReadWriteLock();
Lock readLock = rwLock.readLock();
Lock writeLock = rwLock.writeLock();
读锁是共享的,只要没有写锁,多个读线程可以并发执行。
写锁是独占的,获取写锁时会阻塞所有读写线程。
5.2 典型读写场景示例
class Cache {
private final Map<String, String> map = new HashMap<>();
private final ReadWriteLock rwLock = new ReentrantReadWriteLock();
public String get(String key) {
rwLock.readLock().lock();
try {
return map.get(key);
} finally {
rwLock.readLock().unlock();
}
}
public void put(String key, String value) {
rwLock.writeLock().lock();
try {
map.put(key, value);
} finally {
rwLock.writeLock().unlock();
}
}
}
这种结构在读多写少的场景下,可以显著提升并发吞吐量。
5.3 锁升级被禁止的原因
假设两个线程同时持有读锁,并尝试升级为写锁:
线程 A 等待线程 B 释放读锁
线程 B 等待线程 A 释放读锁
这是典型的死锁场景,因此读写锁从设计上禁止锁升级。
6 Lock 的底层实现逻辑
6.1 AQS 的状态竞争模型
AQS 使用一个 volatile 整型变量表示同步状态,所有线程通过 CAS 操作竞争该状态。
成功修改状态的线程获得锁,失败的线程进入同步队列等待。
6.2 同步队列与条件队列的流转
获取锁失败进入同步队列
await 进入条件队列
signal 将线程转移回同步队列
线程在这两个队列之间的切换,是 Lock 体系支持复杂同步的基础。
7 Lock 与 synchronized 的选择
7.1 必须使用 Lock 的场景
需要超时或可中断获取锁
需要多个条件队列
读写分离
高并发且逻辑复杂
7.2 synchronized 更合适的情况
简单互斥
临界区短
并发风险低
更强调代码稳定性