一、锁的获取与释放(强制规范)
✅ 规则 1:必须使用 try-finally 释放锁
错误示范:
lock.lock();
// 如果这里抛出异常,unlock() 永远不会执行
doSomething();
lock.unlock();
正确示范:
lock.lock();
try {
doSomething();
} finally {
lock.unlock(); // 无论是否异常,都会释放
}
原因:ReentrantLock 是显式锁,不会自动释放。一旦业务代码抛出异常导致 unlock() 被跳过,锁将永久持有,其他线程永远阻塞(死锁)。
✅ 规则 2:lock() 必须写在 try 块之前
错误示范:
try {
lock.lock(); // 如果 lock() 抛出异常(极少见),finally 中的 unlock() 会出问题
doSomething();
} finally {
lock.unlock();
}
正确示范:
lock.lock();
try {
doSomething();
} finally {
lock.unlock();
}
原因:虽然 lock() 几乎不抛异常,但万一发生 Error(如内存溢出),unlock() 可能操作一个未成功持有的锁状态,引发 IllegalMonitorStateException。将 lock() 放在 try 外更安全。
✅ 规则 3:超时获取必须检查返回值
if (lock.tryLock(500, TimeUnit.MILLISECONDS)) {
try {
doSomething();
} finally {
lock.unlock();
}
} else {
// 必须处理获取失败的情况:降级、重试或抛出异常
handleTimeout();
}
原因:tryLock 失败时没有持有锁,若仍然调用 unlock() 会抛出 IllegalMonitorStateException。
二、公平性选择原则
| 场景类型 | 推荐策略 | 理由 |
|---|---|---|
| 高并发、追求吞吐量 | 非公平锁(默认) | 减少上下文切换,吞吐量高 5~10 倍 |
| 任务必须按提交顺序执行 | 公平锁 | 防止线程饥饿,保证 FIFO |
| 混合型(偶尔需要顺序) | 非公平锁 + 额外队列 | 用业务层队列保证顺序,锁仅做互斥 |
示例:保证顺序但不使用公平锁
// 使用 ConcurrentLinkedQueue 维护提交顺序,锁只保护临界区
private final Queue<Runnable> taskQueue = new ConcurrentLinkedQueue<>();
private final ReentrantLock lock = new ReentrantLock();
public void submit(Runnable task) {
taskQueue.offer(task);
lock.lock();
try {
Runnable next = taskQueue.poll();
if (next != null) next.run();
} finally {
lock.unlock();
}
}
三、临界区设计原则
✅ 规则 1:临界区越小越好
// 不好:锁住了耗时 I/O
lock.lock();
try {
String data = readFromFile(); // I/O 操作
processData(data);
} finally {
lock.unlock();
}
// 更好:只锁必要的数据结构操作
String data = readFromFile(); // 无锁读取
lock.lock();
try {
sharedCache.put(key, data); // 仅锁共享数据修改
} finally {
lock.unlock();
}
✅ 规则 2:禁止在临界区内调用未知的外部代码
lock.lock();
try {
// 危险:回调函数可能再次获取同一把锁,导致死锁
listener.onDataChanged(data);
} finally {
lock.unlock();
}
解决:使用锁降级或异步通知,将回调移到锁外执行。
✅ 规则 3:避免嵌套锁,保持锁顺序一致
// 危险:锁顺序不一致可能导致死锁
// 线程1:lockA -> lockB
// 线程2:lockB -> lockA
// 解决:统一锁获取顺序
lockA.lock();
try {
lockB.lock();
try {
// ...
} finally { lockB.unlock(); }
} finally { lockA.unlock(); }
四、Condition 使用规范
✅ 规则 1:必须在 lock 保护区域内使用
lock.lock();
try {
while (conditionNotMet) {
condition.await(); // 正确
}
} finally {
lock.unlock();
}
✅ 规则 2:使用 while 而非 if 检查条件
// 错误:if 无法处理虚假唤醒
if (queue.isEmpty()) {
notEmpty.await();
}
// 正确:while 循环保证条件真正满足
while (queue.isEmpty()) {
notEmpty.await();
}
原因:线程可能被虚假唤醒(spurious wakeup),或被其他线程意外唤醒,必须重新检查条件。
✅ 规则 3:优先使用 signal() 而非 signalAll()
- 当只有一个线程在等待某个条件时,用
signal()精准唤醒,减少惊群效应。 - 当多个线程可能等待同一条件且无法区分时,用
signalAll()确保不丢失唤醒。
✅ 规则 4:await() 后重新获取锁,需注意状态变更
lock.lock();
try {
while (condition) {
condition.await(); // 释放锁并等待
}
// 被唤醒后,锁已重新获取,但共享状态可能已被其他线程修改
// 必须重新评估或读取最新数据
} finally {
lock.unlock();
}
五、避免死锁与饥饿
| 问题 | 解决方案 |
|---|---|
| 死锁(互相等待) | 1. 固定锁获取顺序 2. 使用 tryLock(timeout) 超时放弃3. 使用 lockInterruptibly() 允许外部中断 |
| 饥饿(某些线程永远得不到锁) | 1. 使用公平锁 2. 避免临界区过长导致持锁时间过久 |
| 活锁(线程不断重试却无法进展) | 引入随机退避(如指数退避重试) |
超时避免死锁示例:
while (true) {
if (lock1.tryLock(50, TimeUnit.MILLISECONDS)) {
try {
if (lock2.tryLock(50, TimeUnit.MILLISECONDS)) {
try {
// 执行业务
return;
} finally { lock2.unlock(); }
}
} finally { lock1.unlock(); }
}
// 随机休眠后重试
Thread.sleep(ThreadLocalRandom.current().nextInt(10, 100));
}
六、性能监控与调优
监控方法列表
| 方法 | 用途 |
|---|---|
getQueueLength() | 查看等待线程数,评估锁竞争激烈程度 |
hasQueuedThreads() | 判断是否有线程在排队 |
getHoldCount() | 调试时检查当前线程重入次数 |
isLocked() | 判断锁是否被任意线程持有 |
示例:记录锁竞争情况
if (lock.getQueueLength() > 10) {
logger.warn("锁竞争激烈,等待线程数: {}", lock.getQueueLength());
}
调优建议
- 若
getQueueLength()持续较高,考虑缩小临界区或分段锁(如ConcurrentHashMap思想)。 - 若公平锁吞吐量过低,评估是否真的需要严格顺序,改为非公平锁。
七、与 synchronized 的选择指南
需要以下任一特性时,使用 ReentrantLock:
├── 需要超时获取(tryLock timeout)
├── 需要可中断的锁获取(lockInterruptibly)
├── 需要多个 Condition 精准唤醒
├── 需要公平锁保证顺序
└── 需要监控锁状态(getQueueLength 等)
否则,优先使用 synchronized:
├── 代码简洁,自动释放,不易出错
├── JVM 持续优化(偏向锁、轻量级锁),性能足够
└── 大多数普通同步场景已满足需求
八、常见陷阱速查
| 陷阱 | 后果 | 规避方法 |
|---|---|---|
忘记 unlock() | 死锁 | try-finally 强制释放 |
在 Condition 外调用 await/signal | IllegalMonitorStateException | 确保在 lock/unlock 之间调用 |
使用 if 而非 while 判断条件 | 虚假唤醒导致逻辑错误 | 永远用 while 循环 |
| 公平锁 + 长临界区 | 吞吐量急剧下降 | 缩小临界区或改用非公平锁 |
临界区内调用 Thread.sleep() | 持锁休眠,其他线程全部阻塞 | 将 sleep 移到锁外,或用 Condition.awaitNanos |
| 重入次数溢出 | Error 抛出,程序崩溃 | 避免无限递归加锁 |
总结
ReentrantLock 是强大的同步工具,但“权力越大,责任越大”。遵循上述最佳实践,可以在享受其灵活性的同时,避免死锁、饥饿和性能陷阱。核心原则可概括为:
- 显式锁,显式放 ——
try-finally是铁律。 - 临界区,短平快 —— 只锁必须锁的代码。
- 选公平,需谨慎 —— 吞吐与顺序不可兼得。
- 用条件,要规范 ——
while循环,lock之内。
当不确定是否需要 ReentrantLock 的高级特性时,synchronized 往往是更安全、更简洁的选择。