文章目录

零、引入
“线程唤醒又出问题了!” 王二盯着监控直皱眉,用自定义 AQS 锁改好的库存系统,偶尔出现 “锁释放了但线程没醒” 的情况,库存扣减卡住,用户催单电话被打爆。
领导把他拉到工位前:“AQS 是总控台,LockSupport 就是总控台的‘对讲机’,线程排队全靠它喊,今天必须把这东西搞懂!”
哇哥端着保温杯凑过来:“你以为 AQS 是自己让线程阻塞的?错!AQS 的 CLH 队列里,线程之所以能‘乖乖排队不捣乱’,全靠 LockSupport 的 park/unpark 方法。这东西是 AQS 的手脚,也是 Java 并发的‘底层开关’,今天用门禁系统给你讲透!”
点赞 + 关注,跟着哇哥和王二,吃透 LockSupport 和 AQS 的协作机制,线程阻塞唤醒再也不出错!

一、破案:AQS 的 “唤醒延迟” 坑,根源在 LockSupport

王二的系统里,偶尔出现 “锁释放了,但队列头的线程没醒”,查日志发现是 AQS 调用了唤醒方法,但线程还没进入阻塞状态 —— 这就是 LockSupport 的 “许可机制” 在起作用,而 AQS 正是靠这个机制管理线程。
📢 用 “门禁对讲机” 讲透 LockSupport
LockSupport 是 Java 线程的 “阻塞 / 唤醒开关”,相当于门禁系统的对讲机,总控台(AQS)用它来指挥员工(线程):

“对比之前的 wait/notify,LockSupport 灵活太多了,” 哇哥解释,“wait 必须在 synchronized 里用,而 park 不用;notify 是‘广播喊人’,unpark 是‘点名喊人’,这就是 AQS 能精准唤醒队列头线程的原因。”
二、AQS+LockSupport 的协作流程:线程排队全解析

哇哥打开 AQS 的acquire方法源码,带着王二一步步看 “线程刷卡 - 排队 - 唤醒” 的全流程,每一步都对应门禁场景:

全流程拆解(王二的刷卡经历)
👉 王二刷卡(线程 1 调用 lock ())
- 王二调用lock.lock(),触发 AQS 的acquire(1)方法;
- AQS 调用我们重写的tryAcquire,发现 state=0,用 CAS 改成 1,记录王二为获锁线程;
- 王二进门干活,流程结束(线程 1 获锁成功)。
‼️ 李四刷卡(线程 2 调用 lock ())
- 李四调用lock(),AQS 调用tryAcquire,发现 state=1(王二在里面),获锁失败;
- AQS 把李四的线程包装成 “节点”,用 CAS 加到 CLH 队列末尾(李四站到排队区);
- AQS 调用LockSupport.park(李四),李四的线程阻塞(李四在排队区玩手机)。
✔️ 王二出门(线程 1 调用 unlock ())
- 王二调用lock.unlock(),触发 AQS 的release(1)方法;
- AQS 调用我们重写的tryRelease,把 state 从 1 改成 0,清空获锁线程记录;
- AQS 找到 CLH 队列的头节点(李四),调用LockSupport.unpark(李四)(对讲机喊李四);
- 李四被唤醒,重新调用tryAcquire,CAS 把 state 改成 1,李四进门干活(线程 2 获锁成功)。
✅ 核心源码佐证(AQS 的 acquire 方法)
AQS 的acquire方法是线程抢锁的入口,里面清晰地体现了 “抢锁 - 入队 - 阻塞” 的逻辑:
// AQS的acquire方法(简化版)
public final void acquire(int arg) {
// 1. 尝试抢锁,成功直接返回
if (!tryAcquire(arg) &&
// 2. 抢锁失败,把线程包装成节点加入CLH队列
acquireQueued(addWaiter(Node.EXCLUSIVE), arg)) {
// 3. 阻塞线程(底层调用LockSupport.park)
selfInterrupt();
}
}
// 队列中的线程抢锁逻辑(简化版)
final int acquire(Node node, int arg, boolean shared,
boolean interruptible, boolean timed, long time) {
Thread current = Thread.currentThread();
// ... 太长了, 自己找源码瞅一眼就行了.
}
“看到没?AQS 的arquire方法里,直接调用了 LockSupport.park,” 哇哥指着源码,“这就是 AQS 让线程阻塞的秘密 —— 所有排队的线程,最终都是被 LockSupport‘按住’的。”

三、LockSupport 的核心特性:AQS 的 “精准指挥” 秘诀
王二跟着哇哥写了几个测试用例,彻底摸清了 LockSupport 的特性,这些特性正是 AQS 能精准管理线程的关键。

✔️ 特性 1:许可机制 —— 一次只 “喊” 一个人
LockSupport 的许可(permit)是 “一次性” 的,最多存 1 个,重复 unpark 不会叠加 —— 就像门禁对讲机的 “呼叫权限”,喊一次就用完,再喊得等对方回应。
package cn.tcmeta.aqs;
import java.util.concurrent.locks.LockSupport;
import static java.lang.Thread.sleep;
/**
* @author: laoren
* @description: 测试许可证不能叠加
* @version: 1.0.0
*/
public class LockSupportPermit {
static void main() throws InterruptedException {
Thread worker = new Thread(() -> {
System.out.println("线程:准备等待许可...");
LockSupport.park(); // 消耗1个许可,继续执行
System.out.println("线程:拿到第一个许可");
LockSupport.park(); // 没有许可了,阻塞
System.out.println("线程:拿到第二个许可(不会执行)");
}, "工人线程");
worker.start();
sleep(1000);
// 连续unpark两次,只存1个许可
LockSupport.unpark(worker);
LockSupport.unpark(worker);
System.out.println("主线程:连续发了两个许可");
}
}

AQS 的应用:AQS 唤醒线程时,只会 unpark 一次,不用担心线程 “多拿许可” 导致提前唤醒。
📢 特性 2:unpark 可先于 park—— 解决 “喊早了” 的问题
wait/notify 如果先 notify 再 wait,会导致线程永远等不到唤醒;但 LockSupport 先 unpark 再 park,线程不会阻塞 —— 这解决了 AQS 中 “锁释放时线程还没入队” 的问题。
代码演示:先 unpark 再 park
package cn.tcmeta.aqs;
import java.util.concurrent.locks.LockSupport;
/**
* @author: laoren
* @description: 先 unpark 再 park
* @version: 1.0.0
*/
public class LockSupportOrder {
static void main() {
Thread worker = new Thread(() -> {
try {
Thread.sleep(2000); // 线程先睡2秒,主线程先unpark
System.out.println("线程:准备park...");
LockSupport.park(); // 之前的unpark有效,不会阻塞
System.out.println("线程:顺利执行,没阻塞");
} catch (InterruptedException e) {
e.printStackTrace();
}
}, "工人线程");
worker.start();
// 主线程先unpark
LockSupport.unpark(worker);
System.out.println("主线程:先发许可,再等线程park");
}
}

AQS 的应用: AQS 释放锁时,如果队列还没来得及加节点,unpark 也不会白喊,线程后续 park 时能直接拿到许可。
✨ 特性 3:无需锁关联 —— 比 wait 灵活 10 倍
wait/notify 必须在 synchronized 块里用,否则抛异常;但 LockSupport 的 park/unpark 不用,直接调用就行 —— 这让 AQS 的代码更简洁,不用为了阻塞线程特意加锁。
代码演示:无锁调用 park
package cn.tcmeta.aqs;
import java.util.concurrent.locks.LockSupport;
import static java.lang.Thread.sleep;
/**
* @author: laoren
* @description: 无锁调用 park
* @version: 1.0.0
*/
public class LockSupportNoLock {
static void main() throws InterruptedException {
Thread worker = new Thread(() -> {
System.out.println("线程:无锁park...");
LockSupport.park(); // 不用加锁,直接调用
System.out.println("线程:被唤醒");
}, "工人线程");
worker.start();
sleep(1000);
LockSupport.unpark(worker); // 无锁唤醒
}
}

四、AQS 如何用 LockSupport 解决 “虚假唤醒”?

王二发现,LockSupport 也会出现 “线程自己醒过来” 的虚假唤醒,就像门禁对讲机突然串台,喊了个不存在的人。但 AQS 早就想到了应对方法 —— 用 “死循环 + 业务条件” 兜底。
👉 虚假唤醒演示
package cn.tcmeta.aqs;
import java.util.concurrent.locks.LockSupport;
import static java.lang.Thread.sleep;
/**
* @author: laoren
* @date: 2025/12/9 17:30
* @description: 虚假唤醒演示
* @version: 1.0.0
*/
public class LockSupportSpuriousWakeup {
static void main() throws InterruptedException {
Thread worker = new Thread(() -> {
int count = 0;
while (true) {
System.out.println("线程:第" + (++count) + "次park...");
LockSupport.park(); // 可能虚假唤醒
System.out.println("线程:第" + count + "次被唤醒");
// 检查中断,避免无限循环
if (Thread.interrupted()) {
System.out.println("线程:收到中断,退出");
return;
}
}
}, "易醒线程");
worker.start();
sleep(2000);
LockSupport.unpark(worker); // 正常唤醒
sleep(2000);
worker.interrupt(); // 中断退出
}
}

‼️ AQS 的解决方法:死循环重试
AQS 的acquireQueued方法里,用 for (;😉 死循环让线程被唤醒后重新抢锁,就算虚假唤醒,也会再次检查条件:

是acquire方法的核心循环逻辑,用于线程获取同步状态。其功能可概括为以下几点:
- 检查节点是否为首节点:如果是,则尝试获取资源;否则确保前驱节点有效。
- 队列未初始化则初始化:创建头节点。
- 节点未创建则创建:根据共享/独占模式构造节点。
- 节点未入队则尝试入队:将当前节点加入等待队列。
- 自旋、设置等待状态、挂起线程:通过LockSupport.park阻塞线程,直至被唤醒或超时。
“简单说,就算线程被虚假唤醒,也会回到循环里重新抢锁,抢不到就继续 park ,” 哇哥总结,“这就是 AQS 不怕虚假唤醒的原因 —— 用业务逻辑兜底。”
五、总结: AQS+LockSupport 的核心协作
王二把 AQS 和 LockSupport 的关系总结成 “总控台 + 对讲机” 模型:
- AQS(总控台):管理 CLH 队列,判断线程是否该抢锁、该排队;
- LockSupport(对讲机):执行 AQS 的指令,让排队的线程阻塞,让该唤醒的线程醒来;
- 协作关键:AQS 用死循环 + 业务条件防虚假唤醒,LockSupport 用许可机制保证精准唤醒。
😭 哇哥的血泪彩蛋

“我早年用 LockSupport 写并发工具,没加死循环,” 哇哥捂脸,“结果虚假唤醒导致线程提前干活,把数据库连接池占满了 —— 后来学 AQS 的写法,加了 for (;😉 重试,问题再也没出现。
记住:LockSupport 必须配业务条件判断,不然必踩坑!”
记住:LockSupport 必须配业务条件判断,不然必踩坑!”
记住:LockSupport 必须配业务条件判断,不然必踩坑!”
**关注我,**下一篇我们实战升级,用 AQS+LockSupport 手写一个 Semaphore(信号量),模拟公司门禁的 “多人通行权限”,让你彻底掌握 AQS 的实战用法,面试直接碾压面试官!
✔️ 完结、撒花


看到这了,给自己鼓掌吧~~~~

如果方便的话,关注、点赞、收藏吧~~~

322

被折叠的 条评论
为什么被折叠?



