Java ReentrantLock:从"舔狗式等待"到源码级征服指南
一、ReentrantLock 是什么?
1. 可重入锁的"厕所钥匙"哲学
想象你去网吧上厕所,老板给你一把钥匙(锁),你可以反复进出(可重入),但其他人必须排队。
- 可重入性 :同一线程多次获取锁不会死锁(
synchronized
也是可重入的)。 - 灵活控制:支持公平锁(先来后到)、超时锁(舔狗式等待)、可中断锁(及时止损)。
为什么需要它?
当synchronized
无法满足以下需求时:
- 需要尝试获取锁(
tryLock
) - 需要公平性(防止线程饿死)
- 需要绑定多个条件(Condition)
二、用法大全:从"Hello Lock"到高端操作
1. 基础姿势:加锁与解锁
java
ReentrantLock lock = new ReentrantLock();
lock.lock(); // 获取锁(阻塞直到成功)
try {
// 临界区代码
} finally {
lock.unlock(); // 必须放在finally,否则可能变"死锁侠"
}
注意 :忘记unlock()
比忘记女朋友生日更可怕!
2. 高端操作:Condition的"备胎转正"
java
Condition notFull = lock.newCondition();
Condition notEmpty = lock.newCondition();
// 生产者
public void put(Object item) throws InterruptedException {
lock.lock();
try {
while (queue.size() == capacity) {
notFull.await(); // 队列满时,进入备胎等待区
}
queue.add(item);
notEmpty.signal(); // 唤醒消费者:"有货了!"
} finally {
lock.unlock();
}
}
一个锁可以创建多个Condition ,实现精准唤醒(类似Object的wait/notify
升级版)。
三、原理揭秘:AQS的"舔狗式等待"算法
1. AQS(AbstractQueuedSynchronizer)的核心思想
- 状态(state):表示锁的持有次数(可重入的关键)。
- CLH队列:线程排队等待锁的队列(双向链表结构)。
获取锁流程(非公平模式为例):
- 尝试直接抢锁(CAS修改state)。
- 抢不到则加入队列尾部,进入"自旋检查+阻塞"的舔狗模式。
- 被前驱节点唤醒后再次尝试抢锁。
2. 公平锁 vs 非公平锁
- 非公平锁(默认):新线程可以插队抢锁,吞吐量高,但可能饿死老线程。
- 公平锁:严格按队列顺序分配锁,像地铁排队安检。
源码级对比:
java
// 非公平锁抢锁逻辑
final void lock() {
if (compareAndSetState(0, 1)) // 直接尝试插队
setExclusiveOwnerThread(Thread.currentThread());
else
acquire(1);
}
// 公平锁抢锁逻辑
final void lock() {
acquire(1); // 老老实实排队
}
四、避坑指南:从"死锁地狱"到"锁之大道"
1. 经典死锁场景
java
// 线程A
lock1.lock();
lock2.lock();
// 线程B
lock2.lock();
lock1.lock();
解决方案:
- 顺序加锁:统一先锁A再锁B。
- tryLock超时:舔狗不能无限等待!
java
if (lock1.tryLock(1, TimeUnit.SECONDS)) {
try {
if (lock2.tryLock(1, TimeUnit.SECONDS)) {
// 成功获取双锁
}
} finally {
lock2.unlock();
}
}
2. 锁泄漏
java
lock.lock();
// 中间抛出异常,未执行unlock() → 锁永远无法释放!
正确姿势 :必须把unlock()
放在finally
块中!
五、最佳实践:美团大佬的锁优化秘籍
1. 锁粒度控制
- 细粒度锁:只锁必要代码块(如HashMap的每个桶单独加锁)。
- 锁分段:ConcurrentHashMap用16个Segment减少竞争。
2. 锁命名与监控
java
ReentrantLock lock = new ReentrantLock(true);
lock.setName("OrderLock"); // 自定义锁名,日志排查更友好
// 监控锁竞争情况
if (lock.isHeldByCurrentThread()) {
log.info("锁被线程{}持有", Thread.currentThread().getName());
}
六、面试考点:征服面试官的灵魂拷问
1. ReentrantLock vs synchronized
对比项 | ReentrantLock | synchronized |
---|---|---|
实现方式 | API层面(AQS) | JVM层面(monitor) |
锁释放 | 必须手动unlock() | 自动释放 |
可中断 | 支持(lockInterruptibly) | 不支持 |
公平性 | 可配置 | 非公平 |
性能 | Java 5前优势明显,现在接近 | 优化后差距缩小 |
2. AQS的核心实现
- 模板方法模式 :子类实现
tryAcquire
/tryRelease
。 - CLH队列变体:通过CAS维护等待队列。
- 自旋优化:减少线程切换开销。
高频追问:
- 为什么AQS用双向链表?
方便取消等待节点(如超时或中断时快速移除)。 - state字段如何实现可重入?
每次重入state+1,释放时state-1,归零时完全释放。
七、总结:锁如宝剑,用好了是神兵,用不好是自刎利器
- 选型原则 :优先用
synchronized
,需要高级功能时再选ReentrantLock
。 - 核心口诀 :
- 加锁解锁成对出现(像情侣戒指)。
- 锁粒度要细(像切蛋糕)。
- 死锁预防三招:顺序加锁、超时机制、死锁检测。
最后忠告:
"锁越多,坑越深。
无锁胜有锁,
少锁胜多锁,
万不得已再上锁!"
彩蛋 :
尝试用ReentrantLock
实现一个"舔狗追求系统":
- 女神线程每次回复需等待10秒(
Condition.await(10, SECONDS)
) - 舔狗线程用
tryLock
设置3秒超时,失败后记录"追求失败次数" - 当失败次数超过3次,触发
lockInterruptibly()
及时止损!