1. 从黑盒到透明:ReentrantLock的设计哲学
在Java并发编程的世界里,synchronized关键字就像一台全自动咖啡机——你按下按钮就能获得一杯咖啡,但永远不知道内部的水温控制、压力调节是如何运作的。这种黑盒特性在多线程调试时常常让人抓狂:你不知道哪些线程在等待锁,无法中断一个正在等待的线程,更无法设置获取锁的超时时间。
2004年Java 5引入的ReentrantLock彻底改变了这一局面。它基于Doug Lea大师设计的AQS(AbstractQueuedSynchronizer)框架,将锁机制从JVM的隐秘角落搬到了Java代码的聚光灯下。这种设计就像把咖啡机换成了手冲套装,虽然操作步骤变多了,但你能精确控制水温、注水速度和研磨度。
关键洞察:ReentrantLock不是替代synchronized的工具,而是在需要更精细控制时的专业选择。就像专业摄影师不会只用自动模式拍照一样,高级并发场景需要这种可调控的锁机制。
2. AQS:并发控制的瑞士军刀
2.1 AQS的三维解剖
AQS的核心设计可以用餐厅等位系统来类比理解:
- state字段:相当于餐厅的空桌计数器。0表示没有空桌,1表示有一张空桌,大于1的数字在ReentrantLock中表示锁的重入次数(就像同一个顾客多次加菜)
- exclusiveOwnerThread:记录当前占用锁的线程,相当于餐厅里每张桌上的"已预订"牌
- CLH队列:这是由双向链表实现的等待队列,每个等待线程都被封装成Node节点,相当于餐厅门口的等位名单
// AQS核心结构简化示意 public abstract class AbstractQueuedSynchronizer { private volatile int state; // 核心状态字段 private transient volatile Node head; // 队列头(哨兵节点) private transient volatile Node tail; // 队列尾 static final class Node { volatile Thread thread; volatile Node prev; volatile Node next; volatile int waitStatus; // ... } }2.2 状态机的精妙设计
AQS本质上是一个状态机,其状态转换规则决定了线程的阻塞与唤醒:
- 初始状态:state=0,head=tail=null
- 获取锁成功:state从0→1(或n→n+1表示重入),exclusiveOwnerThread=currentThread
- 释放锁:state从n→n-1,当n-1=0时exclusiveOwnerThread=null
- 等待队列:当获取锁失败时,线程被包装成Node加入CLH队列
这种设计与TCP协议的状态机有异曲同工之妙,都是通过有限状态的变化来管理系统行为。
3. 非公平锁的抢锁艺术
3.1 快速路径:先抢再说
非公平锁(NonfairSync)的加锁逻辑就像地铁早高峰——礼貌排队的人可能永远挤不上车,因为总有人直接从门口插队:
final void lock() { // 第一步:不管三七二十一先尝试CAS抢锁 if (compareAndSetState(0, 1)) { setExclusiveOwnerThread(Thread.currentThread()); } else { acquire(1); // 抢不到再走正规流程 } }这种设计虽然看起来"不道德",但在高并发场景下能显著提升吞吐量。因为刚释放锁的线程有很大概率能立即再次获取锁,避免了线程切换的开销。
3.2 标准流程:AQS的模板方法
当快速抢锁失败后,线程进入AQS的标准处理流程:
public final void acquire(int arg) { if (!tryAcquire(arg) && // 再次尝试获取 acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) // 入队并等待 selfInterrupt(); }这个模板方法定义了获取资源的固定流程,具体实现留给子类完成,是模板方法模式的经典应用。
3.2.1 tryAcquire的实现细节
非公平锁的tryAcquire实现展现了两个关键特性:
final boolean nonfairTryAcquire(int acquires) { final Thread current = Thread.currentThread(); int c = getState(); if (c == 0) { // 锁未被占用 if (compareAndSetState(0, acquires)) { // 再次尝试CAS setExclusiveOwnerThread(current); return true; } } else if (current == getExclusiveOwnerThread()) { // 重入判断 int nextc = c + acquires; if (nextc < 0) throw new Error("Maximum lock count exceeded"); setState(nextc); return true; } return false; }- 非公平性:即使有线程在队列中等待,新来的线程仍然可以尝试抢锁
- 可重入性:通过判断当前线程是否是锁的持有者,并递增state值实现
3.3 排队等待:CLH队列的运作机制
当线程无法立即获取锁时,会被封装成Node加入CLH队列:
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; }CLH队列的三个关键特点:
- 虚拟头节点(哨兵节点)简化边界条件处理
- 入队操作先设置prev指针,再CAS更新tail,最后设置next指针
- 从尾部入队,从头部出队
3.4 自旋与阻塞:性能与公平的平衡
在队列中的线程不会立即阻塞,而是先自旋尝试获取锁:
final boolean acquireQueued(final Node node, int arg) { boolean failed = true; try { boolean interrupted = false; for (;;) { // 自旋循环 final Node p = node.predecessor(); if (p == head && tryAcquire(arg)) { // 只有前驱是头节点时才尝试 setHead(node); p.next = null; // help GC failed = false; return interrupted; } if (shouldParkAfterFailedAcquire(p, node) && parkAndCheckInterrupt()) // 最终挂起线程 interrupted = true; } } finally { if (failed) cancelAcquire(node); } }这个设计有几个精妙之处:
- 前驱检查:只有前驱是头节点时才尝试获取锁,避免所有等待线程同时竞争
- 渐进式阻塞:先自旋几次,实在拿不到锁才调用LockSupport.park()进入阻塞
- 中断处理:在获取锁的过程中响应中断,但不会立即退出
4. 解锁流程:释放与唤醒
4.1 释放锁的核心逻辑
解锁过程是加锁的逆过程,但需要考虑重入的情况:
public final boolean release(int arg) { if (tryRelease(arg)) { // 尝试释放 Node h = head; if (h != null && h.waitStatus != 0) unparkSuccessor(h); // 唤醒后继节点 return true; } return false; } 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); // volatile写保证可见性 return free; }关键点:
- 只有锁的持有者才能释放锁
- state减到0才算完全释放
- 完全释放时需要清空exclusiveOwnerThread
4.2 唤醒机制的精妙设计
唤醒后继节点的过程需要考虑多种边界情况:
private void unparkSuccessor(Node node) { int ws = node.waitStatus; if (ws < 0) compareAndSetWaitStatus(node, ws, 0); // 清除信号 Node s = node.next; if (s == null || s.waitStatus > 0) { // 后继节点无效 s = null; for (Node t = tail; t != null && t != node; t = t.prev) if (t.waitStatus <= 0) // 从后向前找第一个有效节点 s = t; } if (s != null) LockSupport.unpark(s.thread); // 唤醒 }为什么要从后向前遍历?
- 新节点入队时是先设置prev指针,再CAS更新tail,最后设置next指针
- 这种顺序可能导致next指针暂时为空,但prev指针总是可靠的
- 从后向前遍历能确保不会漏掉任何有效节点
5. 公平锁与非公平锁的抉择
5.1 公平性的实现差异
公平锁(FairSync)与非公平锁的核心区别就在tryAcquire的实现:
// 公平锁的tryAcquire 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; } } // ...重入逻辑与非公平锁相同 }hasQueuedPredecessors()方法会检查当前线程是否是队列中的第一个等待线程:
public final boolean hasQueuedPredecessors() { Node t = tail; Node h = head; Node s; return h != t && ((s = h.next) == null || s.thread != Thread.currentThread()); }5.2 性能与公平的权衡
| 特性 | 非公平锁 | 公平锁 |
|---|---|---|
| 吞吐量 | 高(减少线程切换) | 较低 |
| 响应时间 | 不稳定(可能饥饿) | 稳定 |
| 适用场景 | 大多数业务场景 | 需要严格公平性的场景(如计费系统) |
实测数据显示,在高竞争场景下,非公平锁的吞吐量可以是公平锁的2-3倍。这是因为:
- 刚释放锁的线程有很大概率能立即重新获取锁
- 减少了线程挂起和唤醒的开销
- 避免了上下文切换的成本
6. ReentrantLock vs synchronized:如何选择?
6.1 功能对比矩阵
| 特性 | ReentrantLock | synchronized |
|---|---|---|
| 实现层面 | Java代码+AQS | JVM内置 |
| 可中断 | 支持 | 不支持 |
| 超时获取 | 支持 | 不支持 |
| 公平性 | 可配置 | 非公平 |
| 条件变量 | 多个 | 单个 |
| 性能 | Java 6后相当 | Java 6后相当 |
6.2 选型建议
- 需要高级功能时选ReentrantLock:如可中断、超时、公平锁、多个条件变量等
- 简单场景用synchronized:代码更简洁,不易出错
- 性能不再是决定因素:Java 6后的synchronized经过优化,性能差距已经很小
经验法则:就像选择交通工具一样,日常通勤用synchronized(自行车),特殊需求用ReentrantLock(专业赛车)。不要为了用高级特性而增加不必要的复杂度。
7. AQS的设计哲学与扩展应用
AQS的设计体现了几个重要的软件工程原则:
- 模板方法模式:定义算法骨架,具体步骤由子类实现
- 状态与行为分离:state字段管理资源状态,CLH队列管理等待线程
- CAS乐观锁:减少真正的线程阻塞
- 可扩展性:基于AQS可以轻松实现各种同步器(如Semaphore、CountDownLatch)
这种设计使得Java并发包中的各种工具类能够共享同一套高质量的底层实现,避免了重复造轮子。就像城市的基础设施建设,AQS提供了可靠的水电供应,让上层的"建筑"可以专注于业务逻辑的实现。
在实际开发中,理解AQS的工作原理不仅能帮助我们更好地使用ReentrantLock,还能:
- 更准确地诊断死锁和性能问题
- 根据业务特点选择合适的同步策略
- 在必要时实现自定义的同步器
- 编写更高效、更安全的并发代码