JUC并发编程08 - 同步模式/异步模式

同步模式

保护性暂停

单任务版

Guarded Suspension,用在一个线程等待另一个线程的执行结果

  • 有一个结果需要从一个线程传递到另一个线程,让它们关联同一个 GuardedObject
  • 如果有结果不断从一个线程到另一个线程那么可以使用消息队列(见生产者/消费者)
  • JDK 中,join 的实现、Future 的实现,采用的就是此模式

Guarded Suspension 是一种编程模式,用于让一个线程等待另一个线程的执行结果。简单来说,就是“守卫式暂停”。想象一下,你和朋友约定在某个地方见面,但你先到了,你就得在那里等他。等到他来了,你们就可以一起走了。在这个过程中,你就在“守卫式暂停”。

public static void main(String[] args) {
    // 创建 GuardedObject 对象,它就像你和朋友约定的见面地点
    GuardedObject object = new GuardedObjectV2();
    // 启动了一个新线程(假设是你的朋友),让它去完成某个任务(比如买咖啡)
    // 任务完成后,它会调用 object.complete() 方法,把结果(咖啡)放到 GuardedObject 中
    // 并通知你在等待的主线程(你)
    new Thread(() -> {
        sleep(1); // 让这个线程睡1秒,模拟耗时操作
        object.complete(Arrays.asList("a", "b", "c")); // 完成任务,设置结果
    }).start();
    // 主线程调用 object.get(2500) 方法,表示最多等待2.5秒来获取结果
    // 如果在这段时间内结果还没准备好,就返回 null。
    Object response = object.get(2500);
    // 果拿到了结果,就打印出来;如果没有拿到结果,就打印“can't get response”
    if (response != null) {
        log.debug("get response: [{}] lines", ((List<String>) response).size());
    } else {
        log.debug("can't get response");
    }
}

GuardedObject 类详解

/**
    获取结果的方法 get()
*/
public Object get(long millis) {
    synchronized (lock) {
        long begin = System.currentTimeMillis();
        long timePassed = 0;
        while (response == null) {
            long waitTime = millis - timePassed;
            log.debug("waitTime: {}", waitTime);
            if (waitTime <= 0) {
                log.debug("break...");
                break;
            }
            try {
                lock.wait(waitTime);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            timePassed = System.currentTimeMillis() - begin;
            log.debug("timePassed: {}, object is null {}", timePassed, response == null);
        }
        return response;
    }
}
  • synchronized (lock): 确保同一时间只有一个线程能访问这个方法。
  • while (response == null): 如果结果还没准备好,就一直等待。
  • lock.wait(waitTime): 让当前线程暂停,等待其他线程的通知。
  • timePassed: 记录已经等待的时间,防止超时。
/**
    设置结果的方法 complete()
*/
public void complete(Object response) {
    synchronized (lock) {
        this.response = response;
        log.debug("notify...");
        lock.notifyAll();
    }
}
  • synchronized (lock): 确保同一时间只有一个线程能访问这个方法。
  • this.response = response: 把结果存起来。
  • lock.notifyAll(): 通知所有等待的线程,结果已经准备好了。

从一个线程到另一个线程可以使用消息队列

想象你在一个快餐店。

  • 有一个厨师(生产者线程)在不停地做汉堡。
  • 有一群顾客(消费者线程)在等着吃汉堡。

如果每个顾客都傻站着等:“我点的汉堡好了没?”——那太低效了,厨师会被烦死。

所以,快餐店有个“取餐台”(这就是消息队列),厨师做好一个汉堡就放上去,顾客来了就从取餐台拿一个走。

这样:

  • 厨师不用等顾客,做好就放那儿。
  • 顾客也不用一直问,来了看看有没有自己的汉堡就行。

回到程序:

  • 如果你有多个结果要从一个线程传给另一个线程(比如:不断收到网络数据、不断处理任务结果),
  • 那就不适合用 Guarded Suspension(它只等一个结果)。
  • 而应该用消息队列,比如 Java 里的 BlockingQueue
  • 一个线程往队列里放数据(生产者),另一个线程从队列里取(消费者)。

join() 是怎么用 Guarded Suspension 的?

Thread t = new Thread(() -> {
    sleep(3);
    System.out.println("任务完成");
});
t.start();
t.join(); // 主线程在这里等着 t 执行完
System.out.println("t 线程执行完了,我继续...");
  • t.join() 的意思就是:“我(主线程)要等你(t线程)干完活再继续。”
  • 内部是怎么实现的?—— 就是 Guarded Suspension

你可以想象:

  • 每个线程对象里有个“完成标志”(就像 GuardedObject 里的 response)。
  • join() 的时候,主线程检查这个标志:
    • 如果还没完成,就 wait() 等着。
    • 等 t 线程执行完了,JVM 自动调用 notify() 唤醒等它的线程。

所以,join() 本质上就是一个线程在等另一个线程完成,完全符合 Guarded Suspension 的套路。

Future 是怎么用 Guarded Suspension 的?

ExecutorService service = Executors.newCachedThreadPool();
Future<String> future = service.submit(() -> {
    sleep(2);
    return "处理结果";
});

String result = future.get(); // 等结果,可能阻塞
System.out.println("拿到结果:" + result);
  • future.get() 的意思就是:“我现在要拿结果,但如果你还没算完,我就等着。”
  • 它内部也有一个“结果变量”,一开始是空的。
  • 调用 get() 时:
  • 发现结果还没来 → 就 wait() 等着。
  • 另一个线程计算完,把结果塞进去,然后 notify() 唤醒你。

这不就是 GuardedObject 的 get() 和 complete() 吗?

图解:

  • t1: 主线程,等待结果。
  • t2: 新线程,完成任务并设置结果。
  • GuardedObject: 两者共享的对象,用于传递结果。

通过这种方式,我们可以实现线程间的同步和通信。

多任务版

什么是多任务版保护性暂停?

想象一下,你在一个小区里住,每天都有快递员给你送快递。但是,你不能一直站在门口等快递,因为你还有很多事情要做。所以,你需要一个信箱来帮你暂时存放快递,当你有空的时候再去取。

在这个例子中,People 就是你(收件人),Postman 是快递员,Mailboxes 是小区的信箱系统,而 GuardedObject 就是每个具体的信箱。

程序例子

主程序

public static void main(String[] args) throws InterruptedException {
    // 创建3个收件人线程
    for (int i = 0; i < 3; i++) {
        new People().start();
    }
    
    // 等待1秒,让收件人准备好他们的信箱
    Thread.sleep(1000);
    
    // 创建3个快递员线程,分别给对应的收件人送快递
    for (Integer id : Mailboxes.getIds()) {
        new Postman(id, id + "号快递到了").start();
    }
}

收件人(People)

@Slf4j(topic = "c.People")
class People extends Thread{
    @Override
    public void run() {
        // 收信
        GuardedObject guardedObject = Mailboxes.createGuardedObject();
        log.debug("开始收信id:{}", guardedObject.getId());
        
        // 等待快递员送信
        Object mail = guardedObject.get(5000);
        log.debug("收到信id:{},内容:{}", guardedObject.getId(), mail);
    }
}

快递员(Postman)

class Postman extends Thread{
    private int id;
    private String mail;

    public Postman(int id, String mail) {
        this.id = id;
        this.mail = mail;
    }

    @Override
    public void run() {
        // 取出对应的信箱
        GuardedObject guardedObject = Mailboxes.getGuardedObject(id);
        log.debug("开始送信id:{},内容:{}", guardedObject.getId(), mail);
        
        // 把快递放进信箱
        guardedObject.complete(mail);
    }
}

信箱系统(Mailboxes)

class Mailboxes {
    private static Map<Integer, GuardedObject> boxes = new Hashtable<>();
    private static int id = 1;

    // 生成唯一的id
    private static synchronized int generateId() {
        return id++;
    }

    // 获取指定id的信箱
    public static GuardedObject getGuardedObject(int id) {
        return boxes.remove(id);
    }

    // 创建一个新的信箱
    public static GuardedObject createGuardedObject() {
        GuardedObject go = new GuardedObject(generateId());
        boxes.put(go.getId(), go);
        return go;
    }

    // 获取所有信箱的id
    public static Set<Integer> getIds() {
        return boxes.keySet();
    }
}

信箱(GuardedObject)

class GuardedObject {
    private int id;
    private Object response;
    private final Object lock = new Object();

    public GuardedObject(int id) {
        this.id = id;
    }

    public int getId() {
        return id;
    }

    // 获取结果
    public Object get(long millis) {
        synchronized (lock) {
            long begin = System.currentTimeMillis();
            long timePassed = 0;
            while (response == null) {
                long waitTime = millis - timePassed;
                if (waitTime <= 0) {
                    break;
                }
                try {
                    lock.wait(waitTime);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                timePassed = System.currentTimeMillis() - begin;
            }
            return response;
        }
    }

    // 设置结果
    public void complete(Object response) {
        synchronized (lock) {
            this.response = response;
            lock.notifyAll();
        }
    }
}

  • t0, t2, t4: 这些是收件人线程,它们创建自己的信箱并等待快递。
  • t1, t3, t5: 这些是快递员线程,它们找到对应的信箱并把快递放进去。
  • Futures: 这是信箱系统,管理所有的信箱。
  • GO1, GO2, GO3: 这些是具体的信箱,每个信箱都有一个唯一的id。

顺序输出

顺序输出 2 1

package com.cg.jucproject.demo;

import java.util.concurrent.locks.LockSupport;

public class OrderedPrintWithLockSupport {
    public static void main(String[] args) throws InterruptedException {
        // 定义线程 t1:负责打印 "1"
        Thread t1 = new Thread(() -> {
            while (true) {
                /*
                 * 【关键点】LockSupport.park()
                 *
                 * - 当前线程(t1)会在此处暂停,进入阻塞状态。
                 * - 停止的条件是:没有“许可”(permit)。
                 * - 一旦其他线程调用 LockSupport.unpark(t1),t1 就会收到一个“许可”,
                 *   然后 park() 方法返回,t1 继续向下执行。
                 *
                 * 注意:
                 *   1. 如果 unpark(t1) 在 park() 之前调用,许可会提前存在,park() 不会阻塞。
                 *   2. 多次 unpark(t1) 只会保留一个许可(不会叠加)。
                 *
                 * 所以:t1 的执行节奏完全由 t2 的 unpark(t1) 控制。
                 */
                LockSupport.park();

                // 只有被 unpark 唤醒后,才会执行下面这行
                System.out.println("1");
            }
        }, "t1"); // 给线程命名,便于调试

        // 定义线程 t2:负责打印 "2",并唤醒 t1
        Thread t2 = new Thread(() -> {
            while (true) {
                // 1. 先打印 "2"
                System.out.println("2");

                /*
                 * 2. 调用 unpark(t1) 给 t1 发放一个“许可”
                 *
                 * - 这个操作是非阻塞的,立即返回。
                 * - 如果 t1 正在 park() 阻塞,它会被立即唤醒。
                 * - 如果 t1 还没执行到 park(),这个许可会提前存在,
                 *   等 t1 执行 park() 时,发现已有许可,就不会阻塞。
                 *
                 * 关键:这一步确保了 t1 只有在 t2 打印完 "2" 后才能打印 "1"。
                 */
                LockSupport.unpark(t1);

                /*
                 * 3. t2 自己休息 500 毫秒
                 *
                 * - 防止 t2 循环太快,输出过于密集。
                 * - 给 t1 足够时间被唤醒并打印 "1"。
                 * - 同时避免 CPU 空转。
                 */
                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                }
            }
        }, "t2");

        /*
         * 启动两个线程
         *
         * 执行流程分析(按时间顺序):
         *
         * Step 1: t2 先运行(不一定,但逻辑上不影响)
         *   -> 打印 "2"
         *   -> 调用 unpark(t1):给 t1 发放许可
         *   -> t2 进入 sleep(500)
         *
         * Step 2: t1 开始运行
         *   -> 执行 park():但此时已经有许可(由 t2 提前发放)
         *      => 所以 t1 不会阻塞,直接通过
         *   -> 打印 "1"
         *   -> 进入下一轮循环
         *   -> 再次执行 park():此时没有许可(上一个已被消耗)
         *      => t1 阻塞,等待下一个 unpark
         *
         * Step 3: t2 睡醒后继续
         *   -> 打印 "2"
         *   -> unpark(t1):唤醒阻塞的 t1
         *   -> t2 再次 sleep
         *
         * Step 4: t1 被唤醒
         *   -> 打印 "1"
         *   -> 再次 park() -> 阻塞
         *
         * 如此循环,形成稳定输出:
         *     2
         *     1
         *     2
         *     1
         *     ...
         *
         * 保证了输出顺序:2 在前,1 在后
         */

        t1.start();
        t2.start();
    }

    /*
     * LockSupport 是 JDK 提供的底层线程阻塞工具
     * 相比 wait/notify:
     *   - 不需要 synchronized 块
     *   - 不会因通知过早而丢失(unpark 可以在 park 前调用)
     *   - 更简洁、更安全
     */
}
  • LockSupport.park():阻塞当前线程,直到获得许可

  • LockSupport.unpark(thread):为指定线程提供许可,唤醒被阻塞的线程

  • 执行顺序控制:t2先打印"2"并唤醒t1,t1被唤醒后打印"1"

  • 循环机制:两线程都使用无限循环,通过LockSupport实现交替执行

交替输出

连续输出 5 次 abc:

package com.cg.jucproject.demo;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 演示:使用 ReentrantLock + Condition 实现三个线程按顺序循环打印 a b c
 * 输出效果:a b c a b c ... (共5轮)
 */
public class day2_14 {
    public static void main(String[] args) throws InterruptedException {
        // 创建 AwaitSignal 对象,指定循环次数为 5
        AwaitSignal awaitSignal = new AwaitSignal(5);

        // 为每个线程创建独立的 Condition 条件变量
        Condition a = awaitSignal.newCondition(); // 控制线程A(打印"a")
        Condition b = awaitSignal.newCondition(); // 控制线程B(打印"b")
        Condition c = awaitSignal.newCondition(); // 控制线程C(打印"c")

        // 启动三个线程,每个线程负责打印一个字符,并唤醒下一个
        new Thread(() -> {
            awaitSignal.print("a", a, b); // 打印"a",由a控制,唤醒b
        }, "t1").start();

        new Thread(() -> {
            awaitSignal.print("b", b, c); // 打印"b",由b控制,唤醒c
        }, "t2").start();

        new Thread(() -> {
            awaitSignal.print("c", c, a); // 打印"c",由c控制,唤醒a
        }, "t3").start();

        // 主线程休眠1秒,确保三个工作线程都已启动并进入 await 状态
        Thread.sleep(1000);

        /*
         * 【关键启动步骤】主线程开始第一个信号
         *
         * 此时三个线程都已执行 lock() 并调用 await(),全部阻塞在各自的 condition 上。
         *
         * 我们需要手动唤醒第一个线程(打印"a"的线程),触发整个流程。
         *
         * 注意:必须先获取锁才能调用 signal()
         */
        awaitSignal.lock();
        try {
            System.out.print("开始:");
            a.signal(); // 唤醒等待在 condition a 上的线程(即打印"a"的线程)
        } finally {
            awaitSignal.unlock(); // 释放锁,让被唤醒的线程能获取锁继续执行
        }
    }
}

/**
 * 自定义锁类,继承 ReentrantLock,用于管理循环打印逻辑
 */
class AwaitSignal extends ReentrantLock {
    private int loopNumber; // 循环打印的次数(每轮每个字母打印一次)

    public AwaitSignal(int loopNumber) {
        this.loopNumber = loopNumber;
    }

    /**
     * 打印方法
     * @param str 当前线程要打印的内容
     * @param condition 当前线程等待的条件变量
     * @param next 下一个要唤醒的条件变量
     */
    public void print(String str, Condition condition, Condition next) {
        // 每个线程会循环 loopNumber 次(即参与 5 轮打印)
        for (int i = 0; i < loopNumber; i++) {
            lock(); // 获取锁(ReentrantLock)
            try {
                /*
                 * 1. 当前线程等待在其对应的 condition 上
                 *    - 线程A:等待在 a 上
                 *    - 线程B:等待在 b 上
                 *    - 线程C:等待在 c 上
                 *
                 * 调用 await() 会:
                 *   - 释放当前持有的锁
                 *   - 进入阻塞状态,直到被 signal() 唤醒
                 *   - 唤醒后重新竞争锁,获取锁后才继续执行
                 */
                condition.await();

                /*
                 * 2. 被唤醒后执行打印
                 *    只有被 signal() 唤醒的线程才能执行到这里
                 */
                System.out.print(str);

                /*
                 * 3. 唤醒“下一个”线程
                 *    - 打印"a"的线程唤醒 b(即打印"b"的线程)
                 *    - 打印"b"的线程唤醒 c(即打印"c"的线程)
                 *    - 打印"c"的线程唤醒 a(即打印"a"的线程)
                 *
                 * 形成闭环唤醒链:a → b → c → a → ...
                 */
                next.signal();

            } catch (InterruptedException e) {
                e.printStackTrace();
            } finally {
                unlock(); // 释放锁,让下一个线程能获取锁
            }
        }
    }
}
阶段事件说明
1三个线程启动每个线程调用 print(),进入 lock()
2线程获取锁,调用 await()释放锁并阻塞在各自的 Condition 上:<br> - t1 阻塞在 a<br> - t2 阻塞在 b<br> - t3 阻塞在 c
3主线程 sleep(1000)确保所有线程都已进入阻塞状态
4主线程 a.signal()唤醒等待在 a 上的线程(t1)
5t1 被唤醒,获取锁打印 "a",然后 b.signal() 唤醒 t2
6t2 被唤醒,获取锁打印 "b",然后 c.signal() 唤醒 t3
7t3 被唤醒,获取锁打印 "c",然后 a.signal() 唤醒 t1
8回到 t1,第二轮开始循环继续,直到每轮打印 5 次
机制作用
ReentrantLock提供可重入锁,保证同一时间只有一个线程执行打印
Condition每个线程有独立的等待条件,避免误唤醒
await()释放锁并阻塞,直到被 signal() 唤醒
signal()唤醒指定 Condition 上的一个线程
next.signal()实现“链式唤醒”,形成顺序执行

异步模式

传统版

异步模式之生产者/消费者:

package com.cg.jucproject.demo;

import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * 演示:传统版生产者/消费者模式(Producer-Consumer Pattern)
 * 使用 ReentrantLock + Condition 实现线程间通信
 * 场景:一个生产者线程生产,一个消费者线程消费,共享一个变量 number
 */
class ShareData {
    private int number = 0;                    // 共享资源:当前产品数量(0表示无产品,1表示有待消费)
    private Lock lock = new ReentrantLock();   // 可重入锁,保护共享资源
    private Condition condition = lock.newCondition(); // 条件变量,用于线程等待/唤醒

    /**
     * 生产方法:生产一个产品(number 从 0 → 1)
     */
    public void increment() throws Exception {
        lock.lock(); // 获取锁,确保同一时间只有一个线程能执行此方法
        try {
            /*
             * 【1. 判断】是否能生产?
             *
             * 使用 while 而不是 if 的原因:
             *   - 防止“虚假唤醒”
             *   - 当多个消费者线程时,signalAll() 会唤醒所有等待线程,
             *     但只有一个能抢到锁,其他线程醒来后必须重新判断条件是否成立
             *
             * 条件:只有当 number == 0 时才能生产
             * 如果 number != 0(已有产品未消费),则当前生产者必须等待
             */
            while (number != 0) {
                System.out.println(Thread.currentThread().getName() + " 进入等待(产品未消费)");
                condition.await(); // 释放锁,进入 condition 的等待队列
            }

            /*
             * 【2. 干活】执行生产操作
             *
             * 此时 number 一定为 0,可以安全生产
             */
            number++;
            System.out.println(Thread.currentThread().getName() + "\t 生产:+1,当前数量:" + number);

            /*
             * 【3. 通知】唤醒所有等待在 condition 上的线程
             *
             * 这里使用 signalAll() 是因为:
             *   - 可能有多个消费者或生产者线程在等待
             *   - 我们不知道谁该被唤醒,所以全部唤醒,由 while 判断决定谁继续执行
             *
             * 注意:signal() 只唤醒一个线程,但在多线程场景下不够安全
             */
            condition.signalAll();

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock(); // 无论成功与否,必须释放锁
        }
    }

    /**
     * 消费方法:消费一个产品(number 从 1 → 0)
     */
    public void decrement() throws Exception {
        lock.lock(); // 获取锁
        try {
            /*
             * 【1. 判断】是否能消费?
             *
             * 条件:只有当 number == 1 时才能消费
             * 如果 number == 0(无产品),则消费者必须等待
             */
            while (number == 0) {
                System.out.println(Thread.currentThread().getName() + " 进入等待(无产品可消费)");
                condition.await(); // 释放锁,进入等待
            }

            /*
             * 【2. 干活】执行消费操作
             */
            number--;
            System.out.println(Thread.currentThread().getName() + "\t 消费:-1,当前数量:" + number);

            /*
             * 【3. 通知】唤醒所有等待线程
             * 可能有多个生产者在等待生产
             */
            condition.signalAll();

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            lock.unlock();
        }
    }
}

/**
 * 主程序:启动一个生产者和一个消费者线程
 */
public class TraditionalProducerConsumer {
    public static void main(String[] args) {
        ShareData shareData = new ShareData();

        // 创建生产者线程 t1:生产 5 次
        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                try {
                    shareData.increment();
                    // 模拟生产耗时
                    // Thread.sleep(100);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }, "生产者-t1").start();

        // 创建消费者线程 t2:消费 5 次
        new Thread(() -> {
            for (int i = 0; i < 5; i++) {
                try {
                    shareData.decrement();
                    // 模拟消费耗时
                    // Thread.sleep(200);
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }, "消费者-t2").start();

        /*
         * 【程序执行流程分析】
         *
         * 初始状态:number = 0
         *
         * Step 1: 生产者 t1 先运行(不一定,但假设它先抢到锁)
         *   -> 判断 number == 0 ,可以生产
         *   -> number++ → 1
         *   -> 打印:生产 +1
         *   -> signalAll() 唤醒所有线程(此时消费者可能在等)
         *   -> 释放锁
         *
         * Step 2: 消费者 t2 开始运行
         *   -> 判断 number == 1 ,可以消费
         *   -> number-- → 0
         *   -> 打印:消费 -1
         *   -> signalAll() 唤醒所有线程
         *   -> 释放锁
         *
         * Step 3: t1 再次生产(第二轮)
         *   -> number == 0 ,生产 → 1
         *   -> ...
         *
         * Step 4: t2 再次消费
         *   -> number == 1 ,消费 → 0
         *   -> ...
         *
         * 如此交替进行,形成:
         *     生产者-t1	 生产:+1,当前数量:1
         *     消费者-t2	 消费:-1,当前数量:0
         *     生产者-t1	 生产:+1,当前数量:1
         *     消费者-t2	 消费:-1,当前数量:0
         *     ...
         *
         * 实现了“生产一个 → 消费一个”的同步协作
         */

        // 主线程不等待,让生产者和消费者完成
        // 实际中可使用 CountDownLatch 等待完成
    }
}
机制说明
ReentrantLock提供可重入锁,保证线程安全
Condition实现线程间精确通信,替代 wait/notify
while 判断防止虚假唤醒,确保条件成立
signalAll()唤醒所有等待线程,避免死锁(尤其在多生产者/消费者时)

改进版

import java.util.LinkedList;
import java.util.concurrent.TimeUnit;

/**
 * 演示:改进版异步模式生产者/消费者模式
 * 使用阻塞队列实现线程间通信,平衡生产和消费的线程资源。
 */
public class DemoD {
    public static void main(String[] args) {
        MessageQueue queue = new MessageQueue(2); // 创建一个容量为2的消息队列

        // 启动3个生产者线程
        for (int i = 0; i < 3; i++) {
            int id = i;
            new Thread(() -> {
                try {
                    queue.put(new Message(id, "值" + id));
                    System.out.println(Thread.currentThread().getName() + ": 已生产消息 -- " + id);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }, "生产者" + i).start();
        }

        // 启动一个消费者线程,持续从队列中取消息
        new Thread(() -> {
            while (true) {
                try {
                    TimeUnit.SECONDS.sleep(1); // 模拟消费耗时
                    Message message = queue.take();
                    System.out.println(Thread.currentThread().getName() + ": 已消费消息 -- " + message.getValue());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "消费者").start();
    }
}

// 消息队列类,用于Java线程之间通信
class MessageQueue {
    private LinkedList<Message> list = new LinkedList<>(); // 消息的队列集合
    private int capacity; // 队列容量

    public MessageQueue(int capacity) {
        this.capacity = capacity;
    }

    /**
     * 获取消息
     *
     * @return 消息对象
     * @throws InterruptedException 如果当前线程被中断,则抛出此异常
     */
    public synchronized Message take() throws InterruptedException {
        // 【1. 判断】检查队列是否为空
        while (list.isEmpty()) {
            System.out.println(Thread.currentThread().getName() + ": 队列为空,消费者线程等待");
            wait(); // 当前线程进入等待状态,释放锁
        }

        // 【2. 干活】从队列头部获取消息并返回
        Message message = list.removeFirst();
        System.out.println(Thread.currentThread().getName() + ": 已消费消息 -- " + message.getValue());

        // 【3. 通知】唤醒所有等待在该对象上的线程
        notifyAll();

        return message;
    }

    /**
     * 存入消息
     *
     * @param message 要存入的消息对象
     * @throws InterruptedException 如果当前线程被中断,则抛出此异常
     */
    public synchronized void put(Message message) throws InterruptedException {
        // 【1. 判断】检查队列是否已满
        while (list.size() == capacity) {
            System.out.println(Thread.currentThread().getName() + ": 队列已满,生产者线程等待");
            wait(); // 当前线程进入等待状态,释放锁
        }

        // 【2. 干活】将消息加入队列尾部
        list.addLast(message);
        System.out.println(Thread.currentThread().getName() + ": 已生产消息 -- " + message.getValue());

        // 【3. 通知】唤醒所有等待在该对象上的线程
        notifyAll();
    }
}

// 消息类
final class Message {
    private int id;
    private String value;

    public Message(int id, String value) {
        this.id = id;
        this.value = value;
    }

    public int getId() {
        return id;
    }

    public String getValue() {
        return value;
    }

    @Override
    public String toString() {
        return "Message{" +
                "id=" + id +
                ", value='" + value + '\'' +
                '}';
    }
}
机制说明
LinkedList实现消息队列,支持先进先出(FIFO)
synchronized确保方法同步,防止并发问题
wait() 和 notifyAll()实现线程间的精确通信
while 循环防止虚假唤醒,确保条件成立

为什么使用 LinkedList

  • LinkedList 是双向链表实现,支持高效的插入和删除操作(O(1) 复杂度)
  • 适合实现队列结构,提供 addLast() 和 removeFirst() 方法

阻塞队列

package com.cg.jucproject.demo;

import java.util.concurrent.*;

/**
 * 演示:阻塞队列 BlockingQueue 的使用
 * 特别聚焦:SynchronousQueue —— 一种“不存储元素”的特殊阻塞队列
 *
 * 核心特点:
 *   - 容量为 0!不存储任何元素
 *   - 生产者线程必须等待消费者线程“手递手”交接数据(hand-off)
 *   - put() 和 take() 必须同时就绪,才能完成数据传递
 *   - 类似“交换机”或“管道”,实现线程间直接通信
 */
public class BlockingQueueSynchronousDemo {

    public static void main(String[] args) {
        // 创建一个单线程的消费者线程池
        ExecutorService consumer = Executors.newFixedThreadPool(1);
        // 创建一个单线程的生产者线程池
        ExecutorService producer = Executors.newFixedThreadPool(1);

        /*
         * 【关键】使用 SynchronousQueue
         *
         * 与 ArrayBlockingQueue、LinkedBlockingQueue 的区别:
         *
         * | 队列类型             | 是否存储元素 | 容量 | 数据传递方式         |
         * |----------------------|--------------|------|------------------------|
         * | Array/LinkedBlockingQueue | 是           | >0   | 生产 → 存 → 消费       |
         * | SynchronousQueue     | 否           | 0    | 生产 ↔ 消费(直接交接) |
         *
         * SynchronousQueue 的 put() 和 take() 必须“配对”执行:
         *   - 如果先调用 put(),生产者会阻塞,直到有消费者调用 take()
         *   - 如果先调用 take(),消费者会阻塞,直到有生产者调用 put()
         *
         * 这是一种“ rendezvous(会面)”机制,确保生产者和消费者直接见面交接
         */
        BlockingQueue<Integer> queue = new SynchronousQueue<>();

        // 提交生产者任务
        producer.submit(() -> {
            try {
                System.out.println("【生产者】开始生产数据...");

                // 模拟生产耗时(例如计算、IO等)
                Thread.sleep(1000);

                System.out.println("【生产者】准备 put(10) 到队列...");

                /*
                 * 【阻塞点】queue.put(10)
                 *
                 * 此时:
                 *   - 队列是空的(且容量为0)
                 *   - 如果没有消费者在等待 take(),put() 会一直阻塞
                 *   - 直到消费者调用 take(),两者“会面”后数据直接传递
                 *
                 * 注意:put() 不会返回,直到交接完成
                 */
                queue.put(10);

                System.out.println("【生产者】数据 10 已成功交接,任务结束。");

            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                System.err.println("【生产者】被中断!");
                e.printStackTrace();
            }
        });

        // 提交消费者任务
        consumer.submit(() -> {
            try {
                System.out.println("【消费者】启动,等待消费...");

                /*
                 * 【阻塞点】Integer result = queue.take()
                 *
                 * 此时:
                 *   - 队列中没有数据(且无法预先存储)
                 *   - 消费者会一直阻塞,直到生产者调用 put()
                 *   - 一旦 put() 发生,take() 立即返回,获取数据
                 */
                Integer result = queue.take();

                System.out.println("【消费者】成功从队列 take() 数据:" + result);

            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
                System.err.println("【消费者】被中断!");
                e.printStackTrace();
            }
        });
    }
}

SynchronousQueue 的核心特性

特性说明
容量为 0不存储任何元素,peek() 永远返回 null
手递手传递(Hand-off)生产者必须等待消费者,直接传递数据
高吞吐、低延迟适合工作窃取、线程池(如 CachedThreadPool
put/take 必须配对任一操作都会阻塞,直到对方就绪
公平性可选支持 FIFO 或非公平(默认非公平)

与其他阻塞队列对比

队列类型存储典型用途
ArrayBlockingQueue有界数组固定线程池,任务队列
LinkedBlockingQueue无界/有界链表Web 服务器请求队列
SynchronousQueue无存储快速交接,Executors.newCachedThreadPool() 底层实现
DelayQueue延迟元素定时任务
PriorityBlockingQueue优先级队列任务优先级调度
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值