【032】AQS 的 “手脚”:LockSupport 如何让线程 “听话排队”?

请添加图片描述

零、引入

“线程唤醒又出问题了!” 王二盯着监控直皱眉,用自定义 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 的实战用法,面试直接碾压面试官!

✔️ 完结、撒花

请添加图片描述
请添加图片描述

看到这了,给自己鼓掌吧~~~~
请添加图片描述
如果方便的话,关注、点赞、收藏吧~~~
请添加图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值