深入理解 ReentrantLock和AQS底层源码
🔒Java 并发编程:深入理解 ReentrantLock 加锁流程与线程通信机制(含顺序打印 ABC 案例)
本文全面剖析 Java 中 ReentrantLock 的加锁原理,重点分析非公平锁的底层流程以及 **AbstractQueuedSynchronizer( AQS )**思想 并通过经典的“顺序打印 ABC”案例讲解
wait/notify
的线程通信机制。同时对比 synchronized 与 Lock 的应用场景及优劣,适合有一定基础的 Java 开发者阅读与收藏。
继承关系图:
- 红色代表:内部类
- 蓝色代表:继承关系
🧠一、ReentrantLock 非公平锁的加锁流程详解
1. ReentrantLock 默认是 非公平锁
当创建 ReentrantLock 实例时候,构造函数初始化默认:NonfairSync(非公平锁)
public ReentrantLock() {sync = new NonfairSync();
}
非公平锁不保证线程获取锁的顺序,可能会“插队”,性能更高,但存在一定“饥饿”风险。
2. 加锁入口 lock()
方法
public void lock() {sync.lock();
}
默认调用 ReentrantLock 内部类 Sync#lock
方法:
final void lock() {if (!initialTryLock())acquire(1);
}
3. 第一次尝试加锁(无等待队列)
final boolean initialTryLock() {Thread current = Thread.currentThread();if (compareAndSetState(0, 1)) { // 尝试获取锁setExclusiveOwnerThread(current); // 设置当前线程为持有者return true;} else if (getExclusiveOwnerThread() == current) {// 当前线程已持有锁,支持重入int c = getState() + 1;setState(c);return true;} else {return false;}
}
- 利用
CAS
修改state
为 1,表示获取到锁 - 如果当前线程是锁的持有者,说明是可重入锁,
state++
- 否则返回
false
,进入下一阶段
4. 获取失败 → 加入等待队列
调用 AQS#acquire(1)
:
public final void acquire(int arg) {if (!tryAcquire(arg)) {acquire(null, arg, false, false, false, 0L);}
}
继续调用 NonfairSync#tryAcquire()
再尝试一次:
protected final boolean tryAcquire(int acquires) {if (getState() == 0 && compareAndSetState(0, acquires)) {setExclusiveOwnerThread(Thread.currentThread());return true;}return false;
}
第二次尝试获取锁失败后,开始调用AQS#acquire
,将当前线程封装为 Node
加入 CLH 队列,
/*** AQS的核心获取锁方法,支持共享和独占模式,支持可中断和超时* * @param node 当前线程对应的节点(可能为null,会在方法内创建)* @param arg 获取锁的参数(通常为1)* @param shared 是否为共享模式(false为独占模式)* @param interruptible 是否可被中断* @param timed 是否有超时限制* @param time 超时的绝对时间(纳秒)* @return 1表示成功获取,负数表示被取消或超时*/
final int acquire(Node node, int arg, boolean shared,boolean interruptible, boolean timed, long time) {Thread current = Thread.currentThread();// 自旋相关变量:用于减少不必要的park/unpark操作byte spins = 0, postSpins = 0; // 重试次数控制,在第一个线程unpark后使用// 状态标记变量boolean interrupted = false; // 是否被中断过boolean first = false; // 当前节点是否是队列中的第一个等待节点Node pred = null; // 当前节点入队后的前驱节点/** 主循环逻辑(无限循环直到获取成功或被取消):* 1. 检查当前节点是否为队列中第一个等待的节点* - 如果是,确保head稳定;否则确保前驱节点有效* 2. 如果是第一个节点或还未入队,尝试获取锁* 3. 如果节点还未创建,创建节点* 4. 如果还未入队,尝试入队一次* 5. 如果从park中唤醒,重试(最多postSpins次)* 6. 如果WAITING状态未设置,设置状态并重试* 7. 否则park当前线程,清除WAITING状态,并检查是否被取消*/for (;;) {// === 第一步:检查节点状态和位置 ===// 如果当前不是第一个节点,且节点已存在,获取前驱节点if (!first && (pred = (node == null) ? null : node.prev) != null &&!(first = (head == pred))) { // 检查是否变成了第一个节点// 前驱节点已被取消(状态 < 0)if (pred.status < 0) {cleanQueue(); // 清理队列中被取消的节点continue; // 重新开始循环} // 前驱节点的prev为null,可能正在初始化else if (pred.prev == null) {Thread.onSpinWait(); // 自旋等待,确保序列化continue;}}// === 第二步:尝试获取锁 ===// 如果是第一个等待节点或还未入队,尝试获取锁if (first || pred == null) {boolean acquired;try {// 根据共享/独占模式调用不同的获取方法if (shared)acquired = (tryAcquireShared(arg) >= 0);elseacquired = tryAcquire(arg);} catch (Throwable ex) {// 获取过程中出现异常,取消当前节点并重新抛出异常cancelAcquire(node, interrupted, false);throw ex;}// === 获取锁成功 ===if (acquired) {// 如果是第一个节点,需要更新head指针if (first) {node.prev = null; // 清除前驱引用head = node; // 设置为新的headpred.next = null; // 断开原head的next引用,帮助GCnode.waiter = null; // 清除等待线程引用// 如果是共享模式,需要唤醒后续等待的共享节点if (shared)signalNextIfShared(node);// 如果之前被中断过,恢复中断状态if (interrupted)current.interrupt();}return 1; // 成功获取锁}}// === 第三步:创建节点 ===if (node == null) { // 节点还未创建,先创建节点再重试入队if (shared)node = new SharedNode(); // 创建共享模式节点elsenode = new ExclusiveNode(); // 创建独占模式节点} // === 第四步:尝试入队 ===else if (pred == null) { // 节点已创建但还未入队node.waiter = current; // 设置等待线程Node t = tail; // 获取当前尾节点node.setPrevRelaxed(t); // 设置前驱节点(使用relaxed模式避免不必要的内存屏障)if (t == null)tryInitializeHead(); // 队列为空,初始化head节点else if (!casTail(t, node)) // CAS尝试设置为新的tailnode.setPrevRelaxed(null); // CAS失败,回退prev设置elset.next = node; // CAS成功,设置原尾节点的next指针} // === 第五步:自旋优化 ===else if (first && spins != 0) { // 如果是第一个节点且还有自旋次数--spins; // 减少自旋次数,减少重新等待时的不公平性Thread.onSpinWait(); // 自旋等待提示} // === 第六步:设置等待状态 ===else if (node.status == 0) { // 节点状态为初始状态node.status = WAITING; // 设置为等待状态,允许被signal唤醒} // === 第七步:阻塞等待 ===else {long nanos;// 计算下次自旋次数:每次左移1位再加1,实现指数增长spins = postSpins = (byte)((postSpins << 1) | 1);if (!timed)// 无超时限制,无限期阻塞LockSupport.park(this);else if ((nanos = time - System.nanoTime()) > 0L)// 有超时限制且还未超时,阻塞指定时间LockSupport.parkNanos(this, nanos);else// 已经超时,退出循环break;// 被唤醒后清除等待状态node.clearStatus();// 检查中断状态if ((interrupted |= Thread.interrupted()) && interruptible)break; // 如果被中断且允许中断响应,退出循环}}// 循环结束表示获取失败(超时或被中断),取消当前节点return cancelAcquire(node, interrupted, interruptible);
}
📌加锁流程图
Thread.lock()│▼
Sync.lock()│├─► NonfairSync#initialTryLock try CAS state == 0 ? ➜ 成功 → 设置 owner → ✅ 加锁成功│└─► 若失败 → 调用 AQS#acquire(1)│├─► NonfairSync#tryAcquire() 再尝试一次获取锁│└─► 失败 AQS#acquire(null, arg, false, false, false, 0L);→ 入队,创建 Node 封装线程│├─► park 当前线程├─► 等待前驱释放锁
🏷️二、ReentrantLock 特性总结
特性 | 说明 |
---|---|
可中断 | 使用 lockInterruptibly() 可响应中断,避免死锁 |
可超时 | 使用 tryLock(timeout) 限时获取锁 |
可重入 | 同一线程可以多次获取同一把锁,不会死锁 |
支持公平锁 | 构造函数传入 true 实现公平加锁 |
条件变量支持 | 可使用 newCondition() 实现类似 wait/notify 的线程通信机制 |
✅打断示例
Thread t1 = new Thread(() -> {try {lock.lockInterruptibly();} catch (InterruptedException e) {System.out.println("被打断了");return;}try {System.out.println("获取到了锁");} finally {lock.unlock();}
});
lock.lock();
t1.start();
Thread.sleep(1000);
t1.interrupt(); // 打断 t1 阻塞状态
✅设置超时时间
if (lock.tryLock(2, TimeUnit.SECONDS)) {// 成功获取锁
} else {// 获取失败,避免永久阻塞
}
✅公平锁使用
ReentrantLock lock = new ReentrantLock(true); // 公平锁,先进先出
📚三、wait/notify 与 Lock 条件变量对比
1. CPU 空轮询问题(不推荐)
static boolean t2runned = false;
Thread t1 = new Thread(() -> {while (!t2runned); // ❌浪费 CPUSystem.out.println("1");
});
Thread t2 = new Thread(() -> {System.out.println("2");t2runned = true;
});
2. 使用 wait()
/ notify()
实现线程通信 ✅
public class test5 {static boolean t2runned = false;static final Object lock = new Object();public static void main(String[] args) {Thread t1 = new Thread(() -> {synchronized (lock) {while (!t2runned) {try {lock.wait(); // 进入等待队列} catch (InterruptedException e) {e.printStackTrace();}}System.out.println("1");}});Thread t2 = new Thread(() -> {synchronized (lock) {System.out.println("2");t2runned = true;lock.notify(); // 唤醒 t1}});t1.start();t2.start();}
}
🎯四、实战:多线程顺序打印 ABC(经典面试题)
public class test6 {public static void main(String[] args) {waitNotify waitNotify = new waitNotify(1, 5);new Thread(() -> waitNotify.print("a", 1, 2)).start();new Thread(() -> waitNotify.print("b", 2, 3)).start();new Thread(() -> waitNotify.print("c", 3, 1)).start();}
}class waitNotify {private int flag;private int loopNum;waitNotify(int flag, int loopNum) {this.flag = flag;this.loopNum = loopNum;}public void print(String str, int waitFlag, int nextFlag) {for (int i = 0; i < loopNum; i++) {synchronized (this) {while (flag != waitFlag) {try {this.wait();} catch (InterruptedException e) {e.printStackTrace();}}System.out.print(str); // 打印对应字符flag = nextFlag; // 更新下一个线程this.notifyAll(); // 唤醒全部线程}}}
}
🧾输出结果:
abcabcabcabcabc
✅五、总结:并发控制核心对比
特性 | synchronized | ReentrantLock |
---|---|---|
可重入 | ✅ | ✅ |
响应中断 | ❌ | ✅ lockInterruptibly() |
可超时 | ❌ | ✅ tryLock(timeout) |
公平/非公平选择 | ❌ | ✅ 可选 |
条件变量 | 一个 | 支持多个 Condition |
性能 | 较低,受限于 JVM 实现 | 高,适合复杂并发逻辑 |
🚀应用场景推荐
-
使用
synchronized
简单快捷,适合代码结构清晰、锁粒度小的情况。 -
使用
ReentrantLock
适合:- 多条件变量
- 超时/中断控制
- 公平加锁需求
- 更复杂的并发控制场景