并发锁
Java 除了 synchronized,还提供了 Lock 体系。显式锁能提供更灵活的能力,例如尝试获取、超时获取、可中断获取、公平锁和多个条件队列。
# 1. Lock 基本用法
Lock lock = new ReentrantLock();
lock.lock();
try {
updateState();
} finally {
lock.unlock();
}
必须在 finally 中释放锁,否则异常会导致锁永远不释放。
# 2. ReentrantLock
ReentrantLock 是可重入锁。
private final ReentrantLock lock = new ReentrantLock();
public void outer() {
lock.lock();
try {
inner();
} finally {
lock.unlock();
}
}
public void inner() {
lock.lock();
try {
// same thread reenter
} finally {
lock.unlock();
}
}
同一线程可以多次获取同一把锁,但也必须释放相同次数。
# 3. tryLock
立即尝试:
if (lock.tryLock()) {
try {
doWork();
} finally {
lock.unlock();
}
} else {
fallback();
}
带超时:
if (lock.tryLock(100, TimeUnit.MILLISECONDS)) {
try {
doWork();
} finally {
lock.unlock();
}
}
适合避免无限等待、降低死锁风险。
# 4. lockInterruptibly
lock.lockInterruptibly();
try {
doWork();
} finally {
lock.unlock();
}
等待锁期间可以响应中断。适合任务取消、超时控制等场景。
# 5. 公平锁与非公平锁
Lock fairLock = new ReentrantLock(true);
| 类型 | 特点 |
|---|---|
| 非公平锁 | 默认,吞吐通常更高,允许插队 |
| 公平锁 | 尽量按等待顺序获取,吞吐可能下降 |
公平锁减少饥饿,但调度成本更高。默认一般使用非公平锁。
# 6. Condition
Condition 是与 Lock 配套的条件队列。
Lock lock = new ReentrantLock();
Condition notEmpty = lock.newCondition();
lock.lock();
try {
while (queue.isEmpty()) {
notEmpty.await();
}
} finally {
lock.unlock();
}
唤醒:
lock.lock();
try {
queue.offer(item);
notEmpty.signal();
} finally {
lock.unlock();
}
一把锁可以创建多个 Condition,适合生产者消费者中区分 notEmpty 和 notFull。
# 7. ReadWriteLock
读写锁允许多个读线程并发,写线程互斥。
ReadWriteLock rw = new ReentrantReadWriteLock();
Lock readLock = rw.readLock();
Lock writeLock = rw.writeLock();
适合读多写少:
读 + 读:可并发
读 + 写:互斥
写 + 写:互斥
如果写很多,读写锁优势会降低。
# 8. StampedLock
StampedLock 提供乐观读。
long stamp = lock.tryOptimisticRead();
double currentX = x;
double currentY = y;
if (!lock.validate(stamp)) {
stamp = lock.readLock();
try {
currentX = x;
currentY = y;
} finally {
lock.unlockRead(stamp);
}
}
适合读非常多、写较少的场景。但 API 更复杂,不可重入,使用要谨慎。
# 9. AQS 简介
很多同步器基于 AQS:AbstractQueuedSynchronizer。
AQS
├─ state:同步状态
└─ CLH 队列:等待线程队列
典型基于 AQS 的类:
ReentrantLockSemaphoreCountDownLatchReentrantReadWriteLock
理解 AQS 有助于理解锁竞争和线程等待队列。
# 10. 锁选型
| 需求 | 推荐 |
|---|---|
| 简单互斥 | synchronized |
| 尝试锁/超时锁/可中断锁 | ReentrantLock |
| 多条件队列 | ReentrantLock + Condition |
| 读多写少 | ReadWriteLock |
| 极读多且能接受复杂 API | StampedLock |
| 单变量原子更新 | Atomic/LongAdder |
# 专家实践与边界
锁的本质是保护共享状态的不变量。选择锁之前,先确认是否真的需要共享可变状态;能用不可变、线程封闭、消息队列解决的问题,通常比加锁更好维护。
共享状态
│
▼
不变量:状态必须保持一致
│
▼
临界区:检查 + 修改
│
▼
锁保护完整临界区
| 锁 | 适合 | 注意点 |
|---|---|---|
synchronized | 简单互斥 | 语义清晰,JVM 优化成熟 |
ReentrantLock | 需要超时、可中断、公平锁 | 必须 finally 解锁 |
ReadWriteLock | 读多写少 | 写锁饥饿和锁升级问题 |
StampedLock | 读多写少且追求性能 | 不可重入,使用复杂 |
锁优化不要从“换锁类型”开始,而要先减少锁范围、降低共享、拆分热点、避免锁内 IO,并用线程 dump 和指标确认竞争点。
# Tips 快问快答
Q:Lock 使用最重要注意什么?
A:必须在 finally 中 unlock。
Q:ReentrantLock 是可重入的吗? A:是,同一线程可以重复获取。
Q:公平锁一定更好吗? A:不一定,公平锁吞吐通常更低。
Q:tryLock 适合什么? A:避免无限等待,做降级或超时控制。
Q:Condition 比 wait/notify 好在哪里? A:一把锁可以有多个条件队列,唤醒更精准。
Q:读写锁适合什么场景? A:读多写少,读操作耗时较明显。
Q:StampedLock 可重入吗? A:不可重入,使用时要谨慎。
Q:AQS 是什么? A:很多锁和同步器的基础框架,维护同步状态和等待队列。