并发3️⃣消息传递:保护性暂停、生产者消费者

1、同步模式:保护性暂停

Guarded Suspension

  • 场景一个线程等待另一个线程的执行结果
  • 实现:线程关联同一个 GuardedObject 对象,用于在线程之间传递结果
    • 单任务版:无限期等待、超时等待。
    • 多任务版:基于单任务版 + 集合管理。
  • JDK 的 join()Future,基于保护性暂停模式实现。

1.1、无限期等待

示例:线程 t1等待线程 t2 的执行结果。

  • result:线程执行结果。

  • getResult():获取执行结果。

    • synchronzied:调用 wait() 的前提是获得对象锁。
    • while:避免虚假唤醒
      • 真唤醒:线程 t2 执行结束后唤醒 t1,t1 获得执行结果
      • 虚假唤醒:其它线程调用 notify()/notifyAll(),t1 仍未获得结果(即被唤醒时条件不满足)
    • 唤醒后进入下一轮循环,null 则继续等待,非 null 则返回结果。
  • setResult():设置执行结果,唤醒等待线程(t1)

    public class GuardedObject {
        private Object result;
    
        public Object getResult() throws InterruptedException {
            synchronized (this) {
                while (result == null) {
                    // 
                    this.wait();
                }
                return result;
            }
        }
    
        public void setResult(Object result) {
            synchronized (this) {
                // 处理完成
                this.result = result;
                this.notifyAll();
            }
        }
    }
    

测试

  • t1:调用 getResult() 尝试获取结果,在结果产生前处于等待(暂停)状态

  • t2:业务处理需耗时 2 秒,处理结束后设置执行结果,设置结果后会唤醒等待线程

    GuardedObject go = new GuardedObject();
    
    new Thread(() -> {
        try {
            LogUtils.debug("尝试获取结果");
            LogUtils.debug(go.getResult());
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    
    }, "t1").start();
    
    new Thread(() -> {
        // 模拟处理结果
        LogUtils.debug("处理结果中");
        SleepUtils.sleepSeconds(2);
        // 处理结束
        LogUtils.debug("处理结束");
        go.setResult("ok");
    }, "t2").start();
    
  • 结果:线程 t1 和 t2 同时开启,2 秒后 t2 执行结束并设置结果,t1 获取并打印结果。

    image-20220410214013089

1.2、超时等待

示例:在原有基础上,添加超时等待时间(mills)

Object getResult(long mills)

1.2.1、方案分析

方案:方法在原有基础上,将 wait() 改为 wait(mills)

  • 分析:假设 t1 的超时等待时间为 5 秒(mills = 5)
    • 假设 t1 在第 4 秒的时候被虚假唤醒,进入下一轮循环,t1 本来最多再等待 1 秒,而实际再会等待 5 秒。
    • 即使 t1 没有被虚假唤醒,等待 5 秒后仍会进入下一轮循环再次等待,没有达到预期效果。
  • 结论
    • 循环体中的 wait(long) 的参数不能是 mills,应是 “剩余等待时间”。
    • 剩余等待时间 = 超时等待时间 - 已等待时间(remain = mills - elapsed)
      • 超时等待时间:方法参数传入
      • 已等待时间:当前时间 - 初始时间(elapsed = now - start)

1.2.2、实现

getResult(long mills) 实现

相比 getResult() 的变化如下

  • 初始时间:startTime

  • 经历时间(已等待时间):初始为 0

  • while

    • 剩余等待时间:每轮循环开始时计算,remain = mills - elapsed。

    • if:若剩余等待时间非正数,则跳出循环不再等待。

    • wait(remainTime):等待剩余时间。

    • 更新已等待时间,now - start。

      public Object getResult(long mills) throws InterruptedException {
          synchronized (this) {
              long startTime = System.currentTimeMillis();
              long elapsedTime = 0;
      
              while (result == null) {
                  long remainTime = mills - elapsedTime;
                  if (remainTime <= 0) {
                      break;
                  }
                  // 唤醒后更新经历时间,进入下一轮循环,null则继续等待,非null则返回结果
                  wait(remainTime);
      
                  elapsedTime = System.currentTimeMillis() - startTime;
              }
              return result;
          }
      }
      

测试

  • t1:调用 getResult(long) 尝试获取结果,在结果产生前处于等待(暂停)状态,若超过指定时间则不再等待。

  • t2:业务处理需耗时 2 秒,处理结束后设置执行结果,设置结果后会唤醒等待线程。

    GuardedObject1 go = new GuardedObject1();
    long mills;
    
    new Thread(() -> {
        try {
            LogUtils.debug("尝试获取结果");
            LogUtils.debug(go.getResult(mills));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    
    }, "t1").start();
    
    new Thread(() -> {
        // 模拟处理结果
        LogUtils.debug("处理结果中");
        SleepUtils.sleepSeconds(2);
        // 处理结束
        LogUtils.debug("处理结束");
        go.setResult("ok");
    }, "t2").start();
    
  • 结果:线程 t1 和 t2 同时开启,2 秒后 t2 执行结束并设置结果。

    • mills 为 3000:最多等待 3 秒,在第 2 秒时获得 t2 的执行结果。

      image-20220410221323291

    • mills 为 1000:最多等待 1 秒,1 秒时未得到结果则不再等待。

      image-20220410221507769

1.3、源码:join()

join() 的实现,体现了保护性暂停模式

区别:保护性暂停是等待另一个线程的处理结果,join() 是等待另一个线程结束(死亡)

  • base:即初始时间(startTime)

  • now:即经历时间(已等待时间,elapsedTime)

  • 参数判断:小于 0 则不合法

    • 等于 0:调用 wait(0) 无限期等待。
    • 大于 0:进入 while 循环
  • while:循环条件为当前线程存活(alive)

    • delay:即剩余等待时间(remainTime)

    • if:若剩余等待时间非正数,则跳出循环不再等待。

    • wait(delay):等待剩余时间。

    • 更新已等待时间,now - base。

      public final synchronized void join(long millis) throws InterruptedException {
          long base = System.currentTimeMillis();
          long now = 0;
      
          if (millis < 0) {
              throw new IllegalArgumentException("timeout value is negative");
          }
          if (millis == 0) {
              while (isAlive()) {
                  wait(0);
              }
          } else {
              while (isAlive()) {
                  long delay = millis - now;
                  if (delay <= 0) {
                      break;
                  }
                  wait(delay);
                  now = System.currentTimeMillis() - base;
              }
          }
      }
      public final void join() throws InterruptedException {
          join(0);
      }
      

1.4、多任务版等待

  • 以上案例中,体现了一个线程等待另一个线程的执行结果。
  • 多任务n 个线程等待获得结果,有对应的 n 个线程处理并设置结果

1.4.1、分析

  • 涉及 n 对线程,需要有 n 个 GuardObject 负责传递结果。
  • 如何使 n 个等待方线程与 n 个处理方线程一一对应
    • 为 GuardObject 设置唯一编号(id)。
    • 等待方处理方通过 id 关联同一个 GuardObject。
  • 创建一个 Futures 类,用于管理多个 GuardObject 对象。
    • 存储 GuardObject:使用 Map 集合,以便根据 id 区分、保证 id 唯一。
    • 创建 GuardObject、生成 id
      • 为避免 id 重复,不能由等待/处理线程创建。
      • 为解耦,可通过工厂模式创建。
    • 提供获取 guardObject、获取所有 id 的方法。

1.4.2、实现:Futures

:在 GuardedObject 中添加 id 属性,以及 getter、构造方法。

  • 存储 GuardObject:使用 Hashtable 保证线程安全。

  • 生成 id:私有静态变量,每次递增 1,线程安全。

  • 创建 GuardObject:存入集合后返回。

  • 获取 GuardObject:从集合中取出并删除(线程只获取一次结果,GuardObject 无需考虑复用)。

  • 获取所有 id:返回 Map 的 keySet。

    public class Futures {
        private static final Map<Integer, GuardedObject> MAP = new Hashtable<>();
        private static int id = 0;
    
        private static synchronized int generateId() {
            return id++;
        }
    
        public static GuardedObject generateGuardObject() {
            GuardedObject guardedObject = new GuardedObject(generateId());
            MAP.put(guardedObject.getId(), guardedObject);
            return guardedObject;
        }
    
        public static GuardedObject getGuardObject(int id) {
            return MAP.remove(id);
        }
    
        public static Set<Integer> getIds() {
            return MAP.keySet();
        }
    }
    

1.4.3、case:快递

线程模拟:定义 Resident 和 Courier 类,继承 Thread 类。

  • Resident:等待结果的线程(居民,等待快递)

  • Courier:执行并设置结果的线程(快递员,派送快递)

    class Resident extends Thread {
        @Override
        public void run() {
            GuardedObject go = Futures.generateGuardObject();
            try {
                // 尝试得到结果
                LogUtils.debug("尝试取快递" + go.getId());
                Object result = go.getResult();
                LogUtils.debug("\t\t取出快递" + result);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
    
    class Courier extends Thread {
        private final int id;
    
        public Courier(int id) {
            this.id = id;
        }
    
        @Override
        public void run() {
            GuardedObject go = Futures.getGuardObject(id);
            LogUtils.debug("派件中" + go.getId());
            SleepUtils.sleepSeconds(2);
            LogUtils.debug("\t派件成功" + go.getId());
            go.setResult(" 编号" + id);
        }
    }
    

测试:模拟 3 个居民,则对应有 3 个快递员

:第一个 for 执行结束后再执行第二个 for,防止NPE。

  • Resident:创建 3 个线程,每个线程开启时都会创建一个 GuardedObject 并尝试获得结果。

  • Courier:获取 Futures 中集合的 KeySet,创建对应个数的线程,耗时 2 秒处理并设置结果。

    int residentCount = 3;
    for (int i = 0; i < residentCount; i++) {
        new Resident().start();
    }
    
    SleepUtils.sleepSeconds(1);
    for (Integer id : Futures.getIds()) {
        new Courier(id).start();
    }
    
  • 结果

    • 同时开启 3 个 Resident 线程进行等待,1 秒后对应 3 个 Courier 线程开始处理业务
    • 2 秒后处理结束并设置结果,与此同时 Resident 获取结果并输出。

    image-20220410233305247

1.5、小结

保护性暂停:一个线程等待另一个线程的执行结果。

  • 实现:线程关联同一个 GuardedObject 对象。
    • 单任务版
      • 无限期等待:wait()
      • 超时等待:wait(remain)
        (remain = mills - elapsed,elapsed = now - start)
    • 多任务版:基于单任务版 + 集合管理。
  • join() 原理:与保护性暂停的区别,在于 while 的判断条件是线程存活(alive)
  • 保护性暂停是线程的一对一关系
    • 单任务版:单个一对一。
    • 多线程版:多个一对一。
  • 若要实现多对应关系,见生产者/消费者模式。

2、异步模式:生产者/消费者

  • 相比保护性暂停模式,生产者/消费者模式支持多对应关系
  • 生产者/消费者中的消息队列用于线程间通信,消息框架(如 Rabbit MQ)用于进程间通信。
  • JDK 的阻塞队列,基于生产者/消费者模式实现。

2.1、模式结构

2.1.1、结构

  • 生产者:产生数据并存储于缓冲区中,无需关注数据的处理细节。

  • 消费者:从缓冲区中获取数据并处理。

  • 缓冲区:通常以队列的数据结构实现(FIFO)

    image-20220411012506539

2.1.2、缓冲区

缓冲区作用

  • 解耦:生产者和消费者不直接互相依赖,而是依赖于缓冲区。
  • 异步处理
    • 保护性暂停:线程同步(阻塞)等待另一个线程处理结果并立即消费。
    • 生产者消费者:生产者无需同步等待结果的处理,将数据存储于缓冲区后即可执行其它任务。
  • 并发:基于异步处理的特性,支持多线程并发。
  • 缓冲:允许数据的产生速度和处理速度不一致。
    • 未处理的数据存储于缓冲区。
    • 达到缓冲容量上限后停止生成数据。

2.2、实现

2.2.1、Message 类

表示线程产生和处理的数据

class Message {
    private int id;
    private Object message;
	// 构造方法
    // toString()、getter
}

2.2.2、MessageQueue 类

  • LinkedList:消息队列(即缓冲区),基于 Java 双向链表实现。

  • capacity:队列容量。

  • get():从缓冲区中取出数据。

    • synchronzied:调用 wait() 的前提是获得对象锁。
    • while:队列空时暂停消费,直到被 Producer 唤醒。
    • removeFirst():头删除,队头元素出队表示消费。
    • notifyAll():唤醒由于队满而暂停生产的 Producer。
  • put():将数据存入缓冲区。

    • synchronzied:同上

    • while:队列满时暂停生产,直到被 Consumer 唤醒。

    • addLast():尾插入,向队尾加入数据。

    • notifyAll():唤醒由于队空而暂停消费的 Consumer。

      public class MessageQueue {
          private final LinkedList<Message> queue;
          private final int capacity;
      
          public MessageQueue(int capacity) {
              queue = new LinkedList<>();
              this.capacity = capacity;
          }
      
          public Message get() throws InterruptedException {
              synchronized (queue) {
                  while (queue.isEmpty()) {
                      LogUtils.debug("队列为空,暂停消费");
                      queue.wait();
                  }
                  Message message = queue.removeFirst();
                  queue.notifyAll();
                  return message;
              }
          }
      
          public void put(Message message) throws InterruptedException {
              synchronized (queue) {
                  while (queue.size() == capacity) {
                      LogUtils.debug("队列已满,暂停生产");
                      queue.wait();
                  }
                  LogUtils.debug("存储信息" + message.getId());
                  queue.addLast(message);
                  queue.notifyAll();
              }
          }
      }
      

2.2.4、测试

示例:缓冲区容量 = 3,生产者个数 = 5,消费者个数 = 2

  • producer:生产数据,存入缓冲区。

  • comsumer:每秒从缓冲区中取出一个数据进行处理。

    int queueCapacity = 3;
    int producerCount = 5;
    int consumerCount = 2;
    
    MessageQueue mq = new MessageQueue(queueCapacity);
    
    for (int i = 0; i < producerCount; i++) {
        Message message = new Message(i, "message" + i);
        new Thread(() -> {
            try {
                mq.put(message);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }, "p" + i).start();
    }
    
    for (int i = 0; i < consumerCount; i++) {
        new Thread(() -> {
            while (true) {
                try {
                    // 每秒处理一次
                    SleepUtils.sleepSeconds(1);
                    LogUtils.debug("处理结果:" + mq.get());
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "c" + i).start();
    }
    

测试

注意各时间节点

  1. 线程同时启动

    • 生产者线程:由于容量 3,有 3 个线程成功存储信息,2 个线程 wait() 等待。
    • 消费者线程:sleep() 睡眠 1 秒。
  2. 第一次 sleep 结束(二者几乎同时发生,因此未必按顺序显示在控制台)

    • 消费者线程:各从队列中取出一个数据进行处理(FIFO)
    • 生产者线程:队列中空出两个位置,剩余 2 个生产者线程存入信息。
  3. 第二次 sleep 结束:各从队列中取出一个数据进行处理(FIFO),队列中剩 1 个元素。

  4. 第二次 sleep 结束

    • c1:比 c2 抢到时间片,从队列中取出最后一个数据进行处理,队列一空。
    • c2:暂停消费。
  5. 第三次 sleep 结束:c1 暂停消费。

    image-20220411022435598

posted @ 2022-03-27 20:27  Jaywee  阅读(74)  评论(0编辑  收藏  举报

👇