很多兄弟一写多线程,脑子里全是 synchronizedLockThread.sleep(),写出来的代码不仅性能拉胯,还时不时给你报个死锁。到了面试,面试官问一句:“如果让你手写一个 RPC 调用的同步等待,你怎么做?”瞬间就懵了。

其实,并发编程根本不需要死记硬背 API。所有的并发难题,底层都逃不出三大经典设计模式。今天,我们就把这些生涩的理论,降维成生活中最接地气的场景,并彻底拆解 JUC 里的两大核武神器(BlockingQueueCountDownLatch),带你彻底打穿并发编程的“任督二脉”!


一、 同步模式之保护性暂停 (Guarded Suspension)

1. 是什么:餐厅点餐与“死等”的盖饭

在多线程世界里,有一种极其常见的一对一强关联场景:线程 A 必须等待线程 B 准备好某个特定的结果,才能继续往下走。

场景降维:你去餐厅点了一份青椒肉丝盖饭。付完钱拿到小票(共享对象)后,你不能去干别的,只能坐在大厅干等(线程阻塞)。直到后厨(另一个线程)把属于你的那份盖饭做好了,喊你的号,你才能端走吃饭。

在实战中,这就是 Future.get() 和所有 RPC 远程调用(如调用微信支付接口)的底层核心思想:发起请求 -> 线程挂起等待 -> 网络结果返回 -> 唤醒线程继续执行。

2. 怎么用:手写一个极简版 RPC 同步等待

我们用最原生的 wait/notify 来还原这个过程:

Java

public class GuardedSuspensionDemo {
    
    // 共享的取餐小票(保护性对象)
    static class GuardedObject {
        private Object response;

        // 获取结果(顾客等饭)
        public synchronized Object get() {
            // 【重点避坑】:必须用 while,绝不能用 if!
            while (response == null) {
                try {
                    System.out.println(Thread.currentThread().getName() + ":盖饭还没好,继续干等...");
                    wait(); // 释放锁,进入等待队列
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
            return response;
        }

        // 产生结果(后厨出餐)
        public synchronized void complete(Object res) {
            this.response = res;
            System.out.println(Thread.currentThread().getName() + ":盖饭做好了!叮!");
            notifyAll(); // 唤醒所有等待的线程
        }
    }

    public static void main(String[] args) throws InterruptedException {
        GuardedObject ticket = new GuardedObject();

        new Thread(() -> {
            Object meal = ticket.get();
            System.out.println(Thread.currentThread().getName() + ":终于吃上了 " + meal);
        }, "顾客").start();

        Thread.sleep(1000); // 模拟做饭耗时

        new Thread(() -> {
            ticket.complete("青椒肉丝盖饭");
        }, "后厨").start();
    }
}
3. 重点细节:为什么必须用 while 而不是 if

原因是:虚假唤醒 (Spurious Wakeup)

如果用 if (response == null),当线程被 notifyAll() 唤醒时,它会直接跳出 if 块往下执行。但此时唤醒你的可能是操作系统的底层信号,或者盖饭其实是被别人拿走了。用 while 可以在唤醒后再次检查条件,如果还是没饭,乖乖回去继续 wait()


二、 异步模式之生产者消费者 (Producer-Consumer)

1. 是什么:快递流水线与“完美解耦”

如果说保护性暂停是“一对一”的死等,那生产者消费者就是多对多的彻底解耦。这是并发编程中最伟大的模式,没有之一。

场景降维:想象一条双 11 的快递流水线。卸货工人(生产者)疯狂往传送带(中间队列)上扔包裹,扔完就走。分拣工人(消费者)站在另一头按自己的节奏拿包裹。传送带满了,卸货工就歇会儿;传送带空了,分拣工就坐着玩手机。

实战:大到分布式的 Kafka、RocketMQ(削峰填谷),小到单机版线程池(调用者提交任务,工作线程执行任务),全都是这个模式的演化。

2. 神器剖析:BlockingQueue 到底聪明在哪?

在 JDK 中,千万别自己去手写 wait/notify 搞队列,Doug Lea 大神早就为你准备好了 BlockingQueue(阻塞队列)

  • 大白话直觉:它就是一条自带红绿灯的智能传送带

  • 底层原理:它内部封装了锁(如 ReentrantLock)和条件变量(Condition)。

    • 当你调用 put() 往里扔数据时,如果队列满了,它会自动把生产者线程挂起(亮红灯),直到消费者拿走一个数据,它才唤醒生产者继续扔。

    • 当你调用 take() 拿数据时,如果队列空了,它会自动把消费者挂起,直到生产者扔进来新数据。

  • 核心意义:开发者完全不需要关心锁的释放与获取,也不用管什么时候该 wait 什么时候该 notify,直接傻瓜式调用即可!

3. 怎么用:一行代码搞定解耦

Java

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;

public class ProducerConsumerDemo {
    public static void main(String[] args) {
        // 容量为 3 的智能传送带 (底层是数组实现的 ArrayBlockingQueue)
        BlockingQueue<String> queue = new ArrayBlockingQueue<>(3);

        // 卸货工人(生产者)
        new Thread(() -> {
            try {
                for (int i = 1; i <= 5; i++) {
                    String parcel = "包裹-" + i;
                    queue.put(parcel); // 队列满了?自动阻塞等红灯!
                    System.out.println("📦 生产了:" + parcel);
                }
            } catch (InterruptedException e) {}
        }, "生产者").start();

        // 分拣工人(消费者)
        new Thread(() -> {
            try {
                while (true) {
                    Thread.sleep(1000); // 消费者动作很慢
                    String parcel = queue.take(); // 队列空了?自动阻塞等红灯!
                    System.out.println("🚚 消费了:" + parcel);
                }
            } catch (InterruptedException e) {}
        }, "消费者").start();
    }
}

三、 同步模式之顺序控制 (Sequential Control)

1. 是什么:4x100 米接力赛的死规矩

多线程天生是交替随机执行的,谁先抢到 CPU 谁先跑。但有些业务必须讲规矩,强行打破这种随机性。

场景降维:田径场上的 4x100 米接力赛。第二棒选手(线程 2)就算体力再好,也绝对不能提前跑。他必须死死盯着第一棒(线程 1),只有交接棒塞到他手里的那一刻,他才能激活狂奔。

实战:系统启动时的依赖加载。比如:线程 B 负责启动定时任务,但它必须等待线程 A 把数据库连接池初始化完毕后才能动弹。

2. 神器剖析:CountDownLatch 的“倒计时”魔法

早期大家喜欢用 boolean flag 配合死循环来做,不仅浪费 CPU 还容易写出 Bug。JUC 包提供了一个极其优雅的组件:CountDownLatch(倒计时锁)

  • 大白话直觉:想象一扇被施了魔法的大门,门上挂着一把带数字的锁(比如数字是 3)。你想出门(await()),就必须站在门后等。只有当外面的人完成任务并按下 3 次减号按钮(countDown()),锁上的数字归零,大门才会轰然打开,你才能冲出去。

  • 底层原理:基于 AQS(AbstractQueuedSynchronizer)实现。它的 state 变量就是那个倒计时数字。调用 countDown() 就是用 CAS 安全地把 state 减 1;调用 await() 的线程会被扔进 AQS 的同步队列里阻塞,直到 state == 0 时,AQS 会自动唤醒队列里的所有挂起线程。

3. 怎么用:优雅实现交接棒

Java

import java.util.concurrent.CountDownLatch;

public class SequentialControlDemo {
    public static void main(String[] args) {
        // 门上的密码锁,初始值为 1(只需要等一个前置任务)
        CountDownLatch baton = new CountDownLatch(1);

        // 线程 B:第二棒
        new Thread(() -> {
            try {
                System.out.println("🏃 第二棒:在起跑线等待接力棒...");
                baton.await(); // 死死堵在门后,直到数字归零
                System.out.println("🏃 第二棒:拿到棒子,疯狂冲刺!");
            } catch (InterruptedException e) {}
        }).start();

        // 线程 A:第一棒
        new Thread(() -> {
            try {
                System.out.println("💨 第一棒:发令枪响,开始奔跑...");
                Thread.sleep(1000); // 跑了 1 秒
                System.out.println("💨 第一棒:交接棒!");
                baton.countDown(); // 按下减号,数字 1 变成 0,大门敞开!
            } catch (InterruptedException e) {}
        }).start();
    }
}
4. 重点细节:为什么不用 Thread.sleep() 硬等?

很多新手遇到顺序依赖,直接在线程 B 里写个 Thread.sleep(2000),假定线程 A 两秒内肯定能跑完。这种代码极其脆弱!如果 A 跑了 3 秒,B 就会拿到脏数据直接空指针报错;如果 A 跑了 0.1 秒,B 还要傻傻多等 1.9 秒,白白浪费 CPU。真正的顺序控制,必须基于如 CountDownLatch 这般精确的信号传递


总结

无论怎么给你绕复杂的业务场景,只要你能把问题套进下面这三套模板里,并发编程对你来说就不再是玄学:

模式名称 核心理念 生活隐喻 JUC 核武神器 避坑指南
保护性暂停 等待特定结果 餐厅点饭死等号 wait() / notify() 必须配合 while 循环使用,防虚假唤醒。
生产者消费者 解耦、削峰填谷 快递传送带 BlockingQueue 严禁使用废弃的 suspend/resume,会导致死锁。
顺序控制 强行规定执行先后 接力跑交接棒 CountDownLatch 严禁使用 Thread.sleep() 靠猜时间来硬等。
Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐