【046】LockSupport 实战封神!线程阻塞唤醒全掌控,王二再也没背锅


在这里插入图片描述


📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌

📙 作者: 编程技术圈(哇哥面试陪跑)
👉 欢迎关注、分享、评论
✔️ 持续分享更多干货内容
🌐🌏🌎➕tcmeta, 欢迎沟通交流

📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌📌


零、引入

王二的脸终于不灰了,反倒透着点红光。上次用 LockSupport 改好线程代码后,领导查日志时多看了他两眼,说了句 “这代码写得像回事”—— 这是王二进公司半年来,第一次被领导夸。

可没高兴两天,新的问题又来了:他用 LockSupport 写的生产者消费者代码,线程偶尔会 “假死”;用 park 处理中断时,没判断状态导致逻辑出错。王二抱着电脑蹲在哇哥工位旁,活像个讨教武功的小徒弟:“哇哥,LockSupport 实战里的坑咋这么多?”

哇哥正对着 AQS 源码啃得入神,闻言抬了抬眼:“光会用 park 和 unpark 是皮毛,实战要管中断、防假死、懂源码,这才是真东西。今天把 LockSupport 的实战大招教给你,以后线程阻塞唤醒的活,你包圆了都没问题。”

点赞 + 关注,跟着哇哥和王二,吃透 LockSupport 实战技巧,从生产者消费者到 AQS 源码,全给你讲透,再也不背锅!

在这里插入图片描述

一、王二的新坑:LockSupport 实战踩的 “两个大雷”

王二写的生产者消费者代码,用 LockSupport 实现,看着没问题,跑久了就出幺蛾子 —— 要么消费者线程假死,要么中断后逻辑紊乱。

👉 王二的 “残次品” 代码(有坑)

在这里插入图片描述

package cn.tcmeta.locksupport;


import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.LockSupport;

/**
 * @author: laoren
 * @description: 王二的坑:LockSupport实战没处理细节,线程假死+中断逻辑乱
 * @version: 1.0.0
 */
public class LockSupportProducerConsumerBad {

    // 仓库(最多存5个商品)
    private static final AtomicInteger STORE = new AtomicInteger(0);
    private static final int MAX_CAPACITY = 5;

    static void main() throws InterruptedException {
        // 生产者线程
        Thread producer = new Thread(() -> {
            while (true) {
                if (STORE.get() < MAX_CAPACITY) {
                    // 生产商品
                    STORE.incrementAndGet();
                    System.out.println("生产者:生产1个商品,库存=" + STORE.get());

                    // 唤醒消费者
                    LockSupport.unpark(consumer); // 坑1:消费者还没初始化就unpark?
                } else {
                    // 库存满了,生产者阻塞
                    System.out.println("生产者:库存满了,等着...");
                    LockSupport.park(); // 坑2:没处理中断,也没判断库存,容易假死
                }
                try {
                    TimeUnit.MILLISECONDS.sleep(100);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        }, "Producer");

        // 消费者线程(这里有问题:producer里先用到了consumer)
        Thread consumer = new Thread(() -> {
            while (true) {
                if (STORE.get() > 0) {
                    // 消费商品
                    STORE.decrementAndGet();
                    System.out.println("消费者:消费1个商品,库存=" + STORE.get());

                    // 唤醒生产者
                    LockSupport.unpark(producer);
                } else {
                    // 库存空了,消费者阻塞
                    System.out.println("消费者:库存空了,等着...");
                    LockSupport.park();
                }
                try {
                    TimeUnit.MILLISECONDS.sleep(150);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        }, "Consumer");

        // 启动线程
        producer.start();
        consumer.start();

        // 主线程等5秒后中断生产者(测试中断逻辑)
        TimeUnit.SECONDS.sleep(5);
        producer.interrupt();
        System.out.println("主线程:中断生产者线程");
    }
    }
}

运行结果:要么空指针,要么假死

王二挠头:“我明明唤醒了啊,怎么还假死?有时候还报空指针?”

哇哥指着代码里的坑,像老中医号脉似的精准:“两个大雷:

  • 线程引用提前使用:生产者里先调用LockSupport.unpark(consumer),但 consumer 线程还没初始化,直接空指针 —— 就像你喊 “3 号病人”,但 3 号还没挂号;
  • park 后没重检条件:生产者因为库存满而 park,被唤醒后没再检查库存,直接生产 —— 万一唤醒的时候库存还是满的,就会超卖;
  • 中断没处理:生产者被中断后,park 唤醒了,但没退出循环,还在死循环生产 —— 就像病人被打扰醒了,还赖在椅子上不走。”

王二挠头:“我明明唤醒了啊,怎么还假死?有时候还报空指针?”

二、用 “快递收发站” 讲透实战核心:唤醒后必须 “验票”

在这里插入图片描述
哇哥拿小区的快递收发站类比,王二一下就懂了:

“生产者是快递员,消费者是取件人,仓库是快递架(最多放 5 个快递)。之前的问题在哪?快递员(生产者)看到架子满了,往地上一坐就等(park),取件人(消费者)取走一个快递,喊一声‘快来’(unpark),快递员不管架子满不满,直接就放快递 —— 这能不出问题吗?”

“那该咋弄?” 王二问。

“唤醒后必须‘验票’—— 也就是重新检查条件,” 哇哥说,“快递员被喊醒后,先看看架子是不是真的有空位,再放快递;取件人被喊醒后,先看看架子上是不是真的有快递,再取。这就是 LockSupport 实战的核心:park 前要检查条件,被唤醒后还要再检查一遍。”

➡️ 实战优化原则(王二记在小本本上)

实战优化原则(王二记在小本本上)

  • 线程引用安全:确保 unpark 时,目标线程已经初始化完成 —— 别喊还没挂号的病人;
  • 条件循环检查:用while循环代替if判断条件,被唤醒后重新检查 —— 醒了先看看情况,再干活;
  • 中断优雅处理:park 唤醒后,判断线程中断状态,必要时退出循环 —— 被打扰了就别硬赖着;
  • 避免重复唤醒:不用频繁 unpark,确保唤醒有意义 —— 别没事就喊病人,打扰人家休息。

哥帮王二改了代码,加了条件循环、中断处理、线程引用安全,跑了一下午都没出问题:

package cn.tcmeta.locksupport;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.concurrent.locks.LockSupport;

// 优化版:LockSupport实战无坑,生产者消费者稳如老狗
public class LockSupportProducerConsumerGood {
    // 仓库(原子类保证线程安全)
    private static final AtomicInteger STORE = new AtomicInteger(0);
    private static final int MAX_CAPACITY = 5;

    public static void main(String[] args) throws InterruptedException {
        // 1. 先初始化线程引用(解决空指针问题)
        Thread producer = null;
        Thread consumer = null;

        // 生产者线程
        Thread finalConsumer = consumer;
        producer = new Thread(() -> {
            while (!Thread.currentThread().isInterrupted()) { // 检查中断,优雅退出
                // 2. 条件循环检查:库存满了就park(唤醒后再检查一遍)
                while (STORE.get() >= MAX_CAPACITY) {
                    System.out.println("生产者:库存满了(" + STORE.get() + "),等着...");
                    java.util.concurrent.locks.LockSupport.park(); // 阻塞

                    // 3. 唤醒后检查中断,要是被中断了就退出
                    if (Thread.currentThread().isInterrupted()) {
                        System.out.println("生产者:被中断了,退出生产");
                        return;
                    }
                }

                // 生产商品
                STORE.incrementAndGet();
                System.out.println("生产者:生产1个,库存=" + STORE.get());

                // 唤醒消费者(确保消费者已初始化)
                if (finalConsumer != null) {
                    LockSupport.unpark(finalConsumer);
                }

                try {
                    TimeUnit.MILLISECONDS.sleep(100);
                } catch (InterruptedException e) {
                    // 捕获中断,设置中断标志,循环会退出
                    Thread.currentThread().interrupt();
                }
            }
        }, "Producer");

        // 消费者线程
        Thread finalProducer = producer;
        consumer = new Thread(() -> {
            while (!Thread.currentThread().isInterrupted()) {
                // 2. 条件循环检查:库存空了就park
                while (STORE.get() <= 0) {
                    System.out.println("消费者:库存空了(" + STORE.get() + "),等着...");
                    LockSupport.park();

                    // 3. 唤醒后检查中断
                    if (Thread.currentThread().isInterrupted()) {
                        System.out.println("消费者:被中断了,退出消费");
                        return;
                    }
                }

                // 消费商品
                STORE.decrementAndGet();
                System.out.println("消费者:消费1个,库存=" + STORE.get());

                // 唤醒生产者
                LockSupport.unpark(finalProducer);

                try {
                    TimeUnit.MILLISECONDS.sleep(150);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        }, "Consumer");

        // 启动线程
        producer.start();
        consumer.start();

        // 主线程等5秒后中断生产者和消费者
        TimeUnit.SECONDS.sleep(5);
        producer.interrupt();
        consumer.interrupt();
        System.out.println("主线程:中断生产者和消费者");
    }
}

在这里插入图片描述
王二盯着日志,感慨道:“原来关键是用 while 循环检查条件,唤醒后再看一遍库存,之前用 if 就掉坑里了。”

“这就是实战和 demo 的区别,” 哇哥说,“demo 里怎么写都没事,生产环境里,线程被唤醒的原因有很多 —— 可能是 unpark,可能是中断,可能是虚假唤醒,所以必须重新检查条件。”

四、深入源码:LockSupport 是 AQS 的 “核心武器”

哇哥突然话锋一转:“你知道 ReentrantLock、CountDownLatch 这些 JUC 工具类,底层是怎么实现线程阻塞唤醒的吗?全是 LockSupport!”

他打开 JDK 源码,找到 AQS的awaitNanos方法:
在这里插入图片描述
“你看,AQS 的核心就是个线程等待队列,” 哇哥指着源码,“当线程竞争锁失败时,AQS 就把线程放进队列,然后调用 LockSupport.park () 阻塞它;当锁释放时,AQS 就把队列头的线程取出来,调用 LockSupport.unpark () 唤醒它 ——LockSupport 就是 AQS 的‘核心武器’。

在这里插入图片描述
“简单说,AQS 是‘线程管理员’,负责排队和调度;LockSupport 是‘执行员’,负责实际的阻塞和唤醒,” 哇哥总结,“懂了 LockSupport,你看 AQS 源码就像看白话文,不然全是天书。”

五、LockSupport 实战场景扩展:线程中断的 “优雅处理”

在这里插入图片描述
王二又问:“之前用 park 处理中断,中断标志是 true,要是我想在中断后继续执行,咋办?”

“用Thread.interrupted()清除中断标志,” 哇哥写了个例子,“这个方法会返回中断状态,然后把标志清掉 —— 就像病人被打扰醒了,护士说‘没事,你继续等’,病人就把‘被打扰’的记录擦掉,接着等。”

package cn.tcmeta.locksupport;

import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.LockSupport;

/**
 * @author: laoren
 * @description: // LockSupport处理中断后,继续执行
 * @version: 1.0.0
 */
public class LockSupportInterruptContinue {

    public static void main(String[] args) throws InterruptedException {
        Thread worker = new Thread(() -> {
            int count = 0;
            while (count < 5) {
                System.out.println("工人线程:第" + (count + 1) + "次等待...");

                // 使用带超时的 park 防止永久阻塞
                LockSupport.parkNanos(TimeUnit.SECONDS.toNanos(10));

                // 检查中断,清除中断标志
                if (Thread.interrupted()) {
                    System.out.println("工人线程:被中断了,但我偏要继续干!");
                    // 中断标志已被清除,下一次循环还能正常执行
                }

                count++;
                try {
                    TimeUnit.MILLISECONDS.sleep(500);
                } catch (InterruptedException e) {
                    System.err.println("睡眠过程中收到中断信号");
                    Thread.currentThread().interrupt(); // 保持中断状态
                }
            }
            System.out.println("工人线程:活干完了,下班!");
        }, "Worker-Thread");

        worker.start();

        // 主线程中断工人线程两次
        TimeUnit.SECONDS.sleep(1);
        worker.interrupt();
        System.out.println("主线程:第一次中断工人线程");

        TimeUnit.SECONDS.sleep(2);
        worker.interrupt();
        System.out.println("主线程:第二次中断工人线程");
    }
}

工人线程:第1次等待...
主线程:第一次中断工人线程
工人线程:被中断了,但我偏要继续干!
工人线程:第2次等待...
主线程:第二次中断工人线程
工人线程:被中断了,但我偏要继续干!
工人线程:第3次等待...
工人线程:第4次等待...
工人线程:第5次等待...
工人线程:活干完了,下班!

王二眼睛亮了:“这比 wait/notify 灵活多了,wait 被中断直接抛异常,还得用 try-catch 包着,LockSupport 想咋处理就咋处理。”

六、面试必问:LockSupport 实战与源码题(附答案)

在这里插入图片描述
哇哥整理了 3 道实战 + 源码的面试题,王二背完直呼 “稳了”:

👉 面试题 1:用 LockSupport 实现生产者消费者模式,关键要注意什么?

答案:
核心是 “条件循环检查 + 中断处理 + 线程安全”,三点缺一不可:

  • 条件循环:用while代替if检查库存(或其他条件),避免唤醒后条件不满足导致逻辑错误;
  • 中断处理:park 唤醒后检查Thread.currentThread().isInterrupted(),必要时优雅退出,别死循环;
  • 线程安全:确保 unpark 时目标线程已初始化,共享资源用原子类或锁保护;
  • 避免无效唤醒:只在条件变化时 unpark(比如生产后唤醒消费者,消费后唤醒生产者),别频繁唤醒。

⚠️ 面试题 2:LockSupport 的 park () 会响应哪些唤醒场景?

答案:
park () 会在 4 种场景下唤醒,实战中要全考虑到:

  • 其他线程调用LockSupport.unpark(当前线程);
  • 其他线程中断当前线程(thread.interrupt());
  • 虚假唤醒(虽然概率极低,但 JVM 可能会莫名唤醒线程,所以必须用 while 循环检查条件);
  • 调用LockSupport.park(Object blocker)后,blocker 被修改(极少用)。

➡️ 面试题 3:AQS 为什么要用 LockSupport,而不用 wait/notify?

答案:
因为 LockSupport 解决了 wait/notify 的三大痛点,适配 AQS 的线程调度需求:

  • 无需同步块:AQS 的线程队列管理不需要 synchronized,LockSupport 不用同步块,契合 AQS 的设计;
  • 精准唤醒:AQS 需要唤醒队列中特定的线程(比如头节点的下一个线程),LockSupport 的 unpark 能精准唤醒,而 notify 是随机的;
  • 唤醒优先于等待:AQS 中可能先把线程放进队列,再释放锁唤醒它,LockSupport 的 “先 unpark 再 park” 支持这种场景,wait/notify 不行。

七、总结:LockSupport 实战封神心法(王二刻在键盘上)

在这里插入图片描述

王二把实战和源码的核心编成顺口溜,贴满了显示器:

  • LockSupport 实战强,生产消费稳当当;
  • while 循环查条件,if 判断易上当;
  • park 唤醒查中断,优雅退出别硬扛;
  • AQS 底层全靠它,阻塞唤醒不慌张;
  • 四种场景能唤醒,实战里面全记详。

✔️ 哇哥的终极彩蛋

“最后送你个面试大招,” 哇哥压低声音,“面试时被问 LockSupport,你先写生产者消费者代码,再打开 AQS 的awaitNanos等方法,说‘你看,JDK 源码都这么用’—— 面试官一准觉得你是深入源码的高手,不是只会用 API 的菜鸟。”

王二点了点头,把优化后的生产者消费者代码部署到测试环境,跑了 24 小时,线程状态稳定,没有一次假死 —— 他终于不再是那个只会背 API 的菜鸟了。

在这里插入图片描述
在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值