锁概念:互斥锁、可重入锁、可重入读写锁、悲观锁、乐观锁

87 阅读5分钟

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 → 重试(或回退)

一句话总结

  • 可重入锁解决“同线程多次进入的安全性与易用性”;
  • 读写锁用并行读提升吞吐,但写冲突时别指望“白嫖”性能;
  • 悲观锁适合高冲突/强一致;乐观锁适合低冲突/追求吞吐;
  • 选型以冲突率、临界区长度、需要的能力为核心指标,辅之以可中断/定时/公平/条件队列等工程化需求。