1. 从抢座位游戏理解ReentrantLock的核心机制
第一次接触ReentrantLock时,我常被其复杂的源码结构所困扰。直到有天在咖啡厅目睹一场"抢座位"游戏,突然理解了锁竞争的本质。想象一个四人桌的空位争夺:第一个到达的人(线程T1)自然落座并获得使用权;当T1还在座位上时,T2到来发现座位被占,便在一旁安静等待;接着T3、T4陆续加入等待队列。这个生活场景完美映射了ReentrantLock的三大核心特性:
- 独占性:同一时刻只有一个线程能持有锁(坐椅子)
- 可重入性:持有者可以重复获取锁(坐稳后可以叠腿)
- 公平性选择:可选择是否按到达顺序分配锁(是否允许插队)
// 典型使用示例 ReentrantLock lock = new ReentrantLock(true); // 公平模式 lock.lock(); try { // 临界区操作 } finally { lock.unlock(); }关键理解:ReentrantLock的源码实现本质是管理两个队列——同步队列(等待锁的线程)和条件队列(调用await的线程)
2. AQS队列的底层数据结构解析
2.1 CLH队列变体的精妙设计
AbstractQueuedSynchronizer(AQS)采用CLH锁队列的变体实现,这种设计源自多处理器系统的自旋锁优化。与原始CLH不同,AQS的节点结构包含:
static final class Node { volatile int waitStatus; // 关键状态标识 volatile Node prev; // 前驱节点 volatile Node next; // 后继节点 volatile Thread thread; // 持有线程 Node nextWaiter; // 条件队列链接 }状态变迁路径值得特别关注:
- 0:初始化状态
- CANCELLED(1):线程取消争抢锁
- SIGNAL(-1):后继节点需要唤醒
- CONDITION(-2):节点在条件队列
- PROPAGATE(-3):共享模式下传播唤醒
2.2 公平锁与非公平锁的入队差异
通过调试模式跟踪lock()调用栈,可以发现关键区别点:
// 公平锁实现 final void lock() { acquire(1); } // 非公平锁实现 final void lock() { if (compareAndSetState(0, 1)) setExclusiveOwnerThread(Thread.currentThread()); else acquire(1); }非公平锁会先尝试直接获取锁(插队行为),失败后才进入队列。这种设计虽然可能造成线程饥饿,但减少了线程切换开销,实测吞吐量能提升30%以上。
3. 锁获取的完整流程拆解
3.1 tryAcquire的模板方法模式
AQS采用模板方法模式,将获取锁的具体逻辑交给子类实现。ReentrantLock中的公平/非公平实现对比:
// 公平锁实现 protected final boolean tryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { if (!hasQueuedPredecessors() && // 关键区别:检查是否有等待线程 compareAndSetState(0, acquires)) { setExclusiveOwnerThread(current); return true; } } // 处理重入逻辑... } // 非公平锁实现 final boolean nonfairTryAcquire(int acquires) { // 省略hasQueuedPredecessors检查 }3.2 addWaiter的节点创建过程
当tryAcquire失败时,线程会被包装为Node加入队列:
private Node addWaiter(Node mode) { Node node = new Node(Thread.currentThread(), mode); Node pred = tail; if (pred != null) { node.prev = pred; if (compareAndSetTail(pred, node)) { pred.next = node; return node; } } enq(node); // 完整入队逻辑 return node; }这里存在经典的"CAS竞争"问题:多个线程同时尝试修改tail指针时,只有一个能成功,其他线程会进入enq()中的自旋重试。
4. 条件变量的实现原理
4.1 条件队列与同步队列的协作
ConditionObject是AQS的内部类,其await()/signal()实现了经典的管程机制。关键点在于:
- 调用await()时,线程会释放锁并进入条件队列
- signal()触发时,将节点从条件队列转移到同步队列
- 只有回到同步队列后,线程才有机会重新竞争锁
public final void await() throws InterruptedException { if (Thread.interrupted()) throw new InterruptedException(); Node node = addConditionWaiter(); // 加入条件队列 int savedState = fullyRelease(node); // 完全释放锁 while (!isOnSyncQueue(node)) { LockSupport.park(this); // 挂起线程 // 被唤醒后检查中断状态... } // 重新竞争锁... }4.2 条件变量的典型使用场景
在生产者-消费者模型中,条件变量能优雅地实现等待/通知机制:
class BoundedBuffer { final Lock lock = new ReentrantLock(); final Condition notFull = lock.newCondition(); final Condition notEmpty = lock.newCondition(); void put(Object x) throws InterruptedException { lock.lock(); try { while (count == items.length) notFull.await(); // 等待不满条件 // 放入元素... notEmpty.signal(); // 通知非空 } finally { lock.unlock(); } } // take方法类似... }5. 解锁过程的源码级分析
5.1 release方法的触发链条
unlock()操作会触发完整的锁释放流程:
public void unlock() { sync.release(1); // 触发AQS的release流程 } public final boolean release(int arg) { if (tryRelease(arg)) { // 子类实现释放逻辑 Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); // 唤醒后继节点 return true; } return false; }5.2 tryRelease的重入计数处理
ReentrantLock需要处理锁重入的情况,因此释放时要确保完全退出:
protected final boolean tryRelease(int releases) { int c = getState() - releases; if (Thread.currentThread() != getExclusiveOwnerThread()) throw new IllegalMonitorStateException(); boolean free = false; if (c == 0) { // 完全释放 free = true; setExclusiveOwnerThread(null); } setState(c); // 更新状态 return free; }6. 性能优化与实战技巧
6.1 锁粒度的选择策略
根据实际场景选择锁范围:
- 粗粒度锁:保护大段代码,实现简单但并发度低
- 细粒度锁:保护最小必要范围,实现复杂但吞吐量高
// 不好的实践:锁范围过大 public synchronized void process() { // 耗时IO操作... // 业务计算... } // 改进方案:只锁必要部分 public void process() { // 无锁IO操作... synchronized(this) { // 仅保护共享变量访问 } }6.2 避免死锁的编码规范
- 固定顺序获取锁:所有线程按相同顺序申请资源
- 设置超时时间:使用tryLock(timeout, unit)方法
- 静态代码分析:使用FindBugs等工具检测潜在死锁
// 危险的多锁获取方式 public void transfer(Account from, Account to) { synchronized(from) { synchronized(to) { // 可能产生死锁 // 转账逻辑... } } } // 安全改进:按hash排序 public void transferSafe(Account from, Account to) { Account first = from.hashCode() < to.hashCode() ? from : to; Account second = first == from ? to : from; synchronized(first) { synchronized(second) { // 转账逻辑... } } }7. 常见问题排查指南
7.1 锁泄漏检测方案
当忘记调用unlock()时,可以使用ThreadMXBean进行检测:
ThreadMXBean bean = ManagementFactory.getThreadMXBean(); long[] threadIds = bean.findDeadlockedThreads(); if (threadIds != null) { ThreadInfo[] infos = bean.getThreadInfo(threadIds); for (ThreadInfo info : infos) { System.out.println("死锁线程: " + info.getThreadName()); for (MonitorInfo monitor : info.getLockedMonitors()) { System.out.println(" 持有锁: " + monitor.getClassName()); } } }7.2 性能瓶颈定位方法
使用JFR(Java Flight Recorder)分析锁竞争:
- 启动JFR记录:
jcmd <pid> JFR.start duration=60s filename=lock.jfr - 使用JDK Mission Visualizer分析:
- 检查
lock-instances面板 - 关注
waiting-threads和blocking-threads统计
- 检查
- 对热点锁考虑:
- 锁分解(Lock Splitting)
- 乐观锁替代
- 读写锁升级
8. 与synchronized的深度对比
8.1 功能特性矩阵
| 特性 | ReentrantLock | synchronized |
|---|---|---|
| 实现机制 | AQS队列 | JVM内置monitor |
| 可中断性 | 支持lockInterruptibly() | 不支持 |
| 公平性选择 | 构造函数指定 | 非公平 |
| 条件变量 | 支持多个Condition | 单个wait/notify |
| 超时机制 | tryLock(timeout, unit) | 不支持 |
| 性能 | 高竞争场景更优 | 低竞争场景更优 |
| 代码控制 | 显式lock/unlock | 自动释放 |
8.2 选型决策树
根据以下路径选择最合适的同步方案:
- 需要高级功能(超时、公平性等)? → 选择ReentrantLock
- 简单的互斥场景? → 优先考虑synchronized
- 读多写少场景? → 考虑ReadWriteLock
- 需要线程协作? → 评估CountDownLatch/CyclicBarrier
经验法则:在JDK1.6+版本中,synchronized经过优化后性能差距已缩小,除非需要特定功能,否则优先使用内置锁