1) 互斥锁的基本语义(锁=临界区的序列化器)
-
目的:在多线程下一次只允许一个线程进入临界区,避免竞态条件。
-
典型实现:
- JVM 级:synchronized(对象监视器),ReentrantLock(AQS)。
- OS/库:互斥量、信号量(1 容量时等价互斥)。
-
内存语义:成功获取同一把锁的解锁/加锁之间,建立 happens-before;即临界区写对后续持锁线程可见。
什么时候优先 synchronized?——临界区简单、锁粒度固定、无需高级特性(中断、超时、多个条件队列、公平性)时,synchronized 最简洁也最安全。
2) 可重入锁(Reentrant / “同一线程可再次获得同一把锁”)
可重入:同一线程重复进入同一把锁不会死锁,锁内部维护“持有线程 + 重入计数”。
2.1ReentrantLock(AQS 实现)
特性
-
可重入:递增计数;退出需匹配次数解锁。
-
可中断:lockInterruptibly() 响应中断。
-
可定时:tryLock(timeout) 防止无限等待。
-
公平/非公平:构造时可选(非公平吞吐高但可能饥饿)。
-
条件队列:newCondition() 支持多个条件变量,比 synchronized 的 “this.wait/notify” 更灵活。
示例
ReentrantLock lock = new ReentrantLock(/* fair? */ false);
Condition notEmpty = lock.newCondition();
void put(E x) throws InterruptedException {
lock.lock();
try {
while (full()) notFull.await(); // 条件等待释放锁
doPut(x);
notEmpty.signal(); // 精准唤醒
} finally { lock.unlock(); }
}
boolean getWithTimeout() throws InterruptedException {
if (!lock.tryLock(200, TimeUnit.MILLISECONDS)) return false;
try { /* 临界区 */ return true; }
finally { lock.unlock(); }
}
2.2synchronized的可重入性
synchronized void a(){ b(); }
synchronized void b(){ /* 同一对象锁,可重入 */ }
同一线程可重入同一监视器;但无超时、中断、条件队列、公平性等高级能力。
3) 可重入读写锁(读多写少场景的吞吐改良)
思想:把锁拆成 read(共享)与 write(独占)。
-
多读可并行,写需独占。
-
写与任何读/写互斥。
3.1ReentrantReadWriteLock
要点
-
可重入:持有写锁的线程可重入写锁且可降级为读锁(先获取读锁再释放写锁);不支持升级(读→写),否则易死锁。
-
公平/非公平:与 ReentrantLock 类似。
-
偏向写/读:实现选择策略以降低饥饿(JDK 默认对写更友好)。
示例:锁降级(推荐做法)
ReentrantReadWriteLock rw = new ReentrantReadWriteLock();
Lock r = rw.readLock();
Lock w = rw.writeLock();
Object data; volatile boolean dirty;
Object read() {
r.lock();
try {
if (!dirty) return data; // 快路径
} finally { r.unlock(); }
w.lock(); // 升级不行,先释放读锁再获取写锁
try {
if (dirty) { data = load(); dirty = false; }
r.lock(); // 降级:持有写锁再获取读锁
} finally { w.unlock(); }
try { return data; }
finally { r.unlock(); }
}
适用:读远多于写、读操作成本不高且可并行。
注意:在写频繁或临界区很短时,读写锁可能不如普通互斥锁(管理开销+上下文切换)。
3.2StampedLock(乐观读)
- 提供 optimistic read:tryOptimisticRead() 返回一个戳,读完用 validate(stamp) 校验期间是否被写打断。
- 写/悲观读拿 stamp;不可重入、不支持条件队列,须谨慎使用。
StampedLock sl = new StampedLock();
long st = sl.tryOptimisticRead();
var x = data.x; var y = data.y;
if (!sl.validate(st)) { // 有写并发,回退到悲观读
st = sl.readLock();
try { x = data.x; y = data.y; } finally { sl.unlockRead(st); }
}
适用:读极多写极少、读区很短且可容忍一次校验回退。
4) 悲观锁 vs 乐观锁(并发控制哲学与实现)
4.1 悲观锁(冲突假设=常见)
-
思想:默认认为会冲突,先上锁再干活。
-
实现:JVM 中的互斥/读写锁;数据库里的 SELECT ... FOR UPDATE、行锁、表锁。
-
优点:语义直观、冲突直接被序列化,失败代价小。
-
缺点:等待/上下文切换开销、可能导致饥饿/死锁、吞吐下降。
数据库例
BEGIN;
SELECT * FROM account WHERE id=1 FOR UPDATE; -- 悲观加锁
UPDATE account SET balance = balance - 100 WHERE id=1;
COMMIT;
4.2 乐观锁(冲突假设=少见)
-
思想:不先加锁,提交时检查是否被别人改过;若冲突重试。
-
实现:
-
CAS:compareAndSet(expected, update)(JUC 原子类、AQS 自旋)。
-
版本号/时间戳:带着 version 更新;若 WHERE id=? AND version=? 受影响行数=0则重试。
-
StampedLock 的 optimistic read 是读路径乐观化的例子。
-
代码例(CAS)
AtomicInteger ai = new AtomicInteger(0);
for (;;) {
int cur = ai.get();
int next = cur + 1;
if (ai.compareAndSet(cur, next)) break; // 冲突则自旋重试
}
数据库例(版本号)
UPDATE product SET stock = stock - 1, version = version + 1
WHERE id = ? AND version = ?; -- 返回0表示被别人改过,需重试
优点:无阻塞、高吞吐、适合读多写少/短临界区。
缺点:冲突重试成本,可能活锁;需要解决 ABA(用 AtomicStampedReference 或版本号)与公平性问题。
5) 选型指南(如何选对锁)
| 诉求 | 推荐 |
|---|---|
| 代码最简、无需特殊能力 | synchronized |
| 需要可中断/定时/公平/多条件 | ReentrantLock |
| 读多写少,读可并发 | ReentrantReadWriteLock |
| 读极多写极少、读区很短、追求极致 | StampedLock(乐观读) |
| 原子更新/计数器/无锁栈队列 | 原子类(CAS,乐观) |
| 数据库并发更新,冲突少 | 版本号乐观锁 |
| 数据库强隔离、冲突高 | 悲观锁(行锁/FOR UPDATE) |
6) 常见问题 & 最佳实践
- 死锁:统一加锁顺序;使用定时/可中断获取;尽量缩短临界区。
- 饥饿/公平性:需要时选择公平锁;但注意公平降低吞吐。
- 条件等待:用 while 不是 if 重新校验条件(防虚假唤醒)。
- 读写锁升级:不要读→写直接升级;使用“释放读锁→获取写锁”,或改用单锁。
- 锁降级:写→读可按顺序先获取读锁再释放写锁,保证数据视图稳定。
- 性能诊断:避免在临界区进行 I/O 或阻塞操作;使用探针(JFR/Async-profiler)定位锁争用。
- ABA 问题(乐观锁/CAS):使用带版本(AtomicStampedReference)或逻辑时间戳。
- 内存可见性:锁天然提供;如果用乐观方案(CAS + volatile),确保必要字段是 volatile 或通过发布-订阅建立可见性。
7) 速查代码片段
ReentrantLock(可中断 + 定时)
if (lock.tryLock(200, TimeUnit.MILLISECONDS)) {
try { /* work */ }
finally { lock.unlock(); }
} else {
// 降级路径/放弃
}
ReentrantReadWriteLock(读多写少)
rw.readLock().lock();
try { read(); }
finally { rw.readLock().unlock(); }
rw.writeLock().lock();
try { write(); dirty = false; }
finally { rw.writeLock().unlock(); }
StampedLock(乐观读 + 回退)
long s = sl.tryOptimisticRead();
var v = snapshot();
if (!sl.validate(s)) {
s = sl.readLock();
try { v = snapshot(); } finally { sl.unlockRead(s); }
}
return v;
数据库版本号乐观锁
-- 首次读取得到 version=v
UPDATE t SET val=?, version=version+1 WHERE id=? AND version=?;
-- rows=0 → 重试(或回退)
一句话总结
- 可重入锁解决“同线程多次进入的安全性与易用性”;
- 读写锁用并行读提升吞吐,但写冲突时别指望“白嫖”性能;
- 悲观锁适合高冲突/强一致;乐观锁适合低冲突/追求吞吐;
- 选型以冲突率、临界区长度、需要的能力为核心指标,辅之以可中断/定时/公平/条件队列等工程化需求。