通过实现生产者消费者再次案例实践Java-多线程

43次阅读

共计 5785 个字符,预计需要花费 15 分钟才能阅读完成。

线程通信,在多线程系统中,不同的线程执行不同的任务;如果这些任务之间存在联系,那么执行这些任务的线程之间就必须能够通信,共同协调完成系统任务。

生产者、消费者案例

案例分析

在案例中明,蔬菜基地作为生产者,负责生产蔬菜,并向超市输送生产的蔬菜;消费者通过向超市购买获得蔬菜;超市怎作为生产者和消费者之间的共享资源,都会和超市有联系;蔬菜基地、共享资源、消费者之间的交互流程如下:

在这个案例中,为什么不设计成生产者直接与给消费者交互?让两者直接交换数据不是更好吗,选择先先把数据存储到共享资源中,然后消费者再从共享资源中取出数据使用,中间多了一个环节不是更麻烦了?

其实不是的,设计成这样是有原因的,因为这样设计很好的体现了面向对象的 低耦合的设计理念;通过这样实现的程序能更加符合人的操作理念,更加贴合现实环境;同时,也能很好的避免因生产者与消费者直接交互而导致的操作不安全的问题。

我们来对 高耦合和低耦合 做一个对比就会很直观了:

  • 高 (紧) 耦合:生产者与消费者直接交互,生产者(蔬菜基地)把蔬菜直接给到给消费者,双方之间的依赖程度很高;此时,生产者中就必须持有消费者对象的引用,同样的道理,消费者也必须要持有生产者对象的引用;这样,消费者和生产者才能够直接交互。
  • 低 (松) 耦合: 引入一个 中间对象——共享资源 来,将生产者、消费者中需要对外输出或者从外数据的操作封装到中间对象中,这样,消费者和生产者将会持有这个中间对象的引用,屏蔽了生产者和消费者直接的数据交互.,大大见减小生产者和消费者之间的依赖程度。

关于高耦合和低耦合的区别,电脑中主机中的集成显卡和独立显卡也是一个非常好的例子。

  • 集成显卡 普遍都集成于 CPU 中,所以如果集成显卡出现了问题需要更换,那么会连着 CPU 一块更换,其维护成本与 CPU 其实是一样的;
  • 独立显卡 需要插在主板的显卡接口上才能与计算机通信,其相对于整个计算机系统来说,是独立的存在,即便出现问题需要更换,也只更换显卡即可。

案例的代码实现

接下来我们使用多线程技术实现该案例,案例代码如下:

蔬菜基地对象,VegetableBase.java

// VegetableBase.java

// 蔬菜基地
public class VegetableBase implements Runnable {

    // 超市实例
    private Supermarket supermarket = null;

    public VegetableBase(Supermarket supermarket) {this.supermarket = supermarket;}

    @Override
    public void run() {for (int i = 0; i < 100; i++) {if (i % 2 == 0) {supermarket.push("黄瓜", 1300);
                System.out.println("push : 黄瓜" + 1300);
            } else {supermarket.push("青菜", 1400);
                System.out.println("push : 青菜" + 1400);
            }
        }
    }
}

消费者对象,Consumer.java

// Consumer.java

// 消费者
public class Consumer implements Runnable {

    // 超市实例
    private Supermarket supermarket = null;

    public Consumer(Supermarket supermarket) {this.supermarket = supermarket;}

    @Override
    public void run() {for (int i = 0; i < 100; i++) {supermarket.popup();
        }
    }
}

超市对象,Supermarket.java

// Supermarket.java

// 超市
public class Supermarket {

    // 蔬菜名称
    private String name;
    // 蔬菜数量
    private Integer num;

    // 蔬菜基地想超市输送蔬菜
    public void push(String name, Integer num) {
        this.name = name;
        this.num = num;
    }

    // 用户从超市中购买蔬菜
    public void popup() {
        // 为了让效果更明显,在这里模拟网络延迟
        try {Thread.sleep(1000);
        } catch (InterruptedException e) { }
        System.out.println("蔬菜:" + this.name + "," + this.num + "颗。");
    }

}

运行案例,App.java

// 案例应用入口
public class App {public static void main(String[] args) {
        // 创建超市实例
        Supermarket supermarket = new Supermarket();
        // 蔬菜基地线程启动, 开始往超市输送蔬菜
        new Thread(new VegetableBase(supermarket)).start();
        new Thread(new VegetableBase(supermarket)).start();
        // 消费者线程启动,消费者开始购买蔬菜
        new Thread(new Consumer(supermarket)).start();
        new Thread(new Consumer(supermarket)).start();}

}

发现了问题

运行该案例,打印出运行结果,外表一片祥和,可还是被敏锐的发现了问题,问题如下所示:

在一片看似祥和的打印结果中,出现了一个很不祥和的特例,生产基地在输送蔬菜时,黄瓜的数量一直都是 1300 颗,青菜的数量一直是 1400 颗,但是在消费者消费时却出现了 蔬菜名称是黄瓜的,但数量却是青菜的数量 的情况。

之所以出现这样的问题,是因为在本案例共享的资源中,多个线程共同竞争资源时没有使用 同步操作 ,而是异步操作,今儿导致了资源分配紊乱的情况;需要注意的是, 并不是因为我们在案例中使用 Thread.sleep(); 模拟网络延迟才导致问题出现,而是本来就存在问题,使用 Thread.sleep(); 只是让问题更加明显。

案例问题的解决

在本案例中需要 解决的问题有两个,分别如下:

  1. 问题一: 蔬菜名称和数量不匹配的问题。
  2. 问题二: 需要保证超市无货时生产,超市有货时才消费。

针对 问题一解决方案:保证蔬菜基地在输送蔬菜的过程保持同步,中间不能被其他线程(特别是消费者线程)干扰,打乱输送操作;直至当前线程完成输送后,其他线程才能进入操作,同样的,当有线程进入操作后,其他线程只能在操作外等待。

所以,技术方案 可以使用 同步代码块 / 同步方法 /Lock 机制 来保持操作的同步性。

针对 问题二的解决方案:给超市一个有无货的状态标志,

  • 超市无货时,蔬菜基地输送蔬菜补货,此时生产基地线程可操作;
  • 超市有货时,消费者线程可操作;就是:保证生产基地 ——> 共享资源 ——> 消费者 这个整个流程的完整运行。

技术方案 :使用线程中的 等待和唤醒机制

同步操作,分为 同步代码块 同步方法 两种。详情可查看我的另外一篇关于多线程的文章:「JAVA」Java 线程不安全分析,同步锁和 Lock 机制,哪个解决方案更好

  1. 在同步代码块中的 同步锁 必须选择 多个线程共同的资源对象 ,当前生产者线程在生产数据的时候(先 拥有同步锁 ),其他线程就在锁池中等待获取锁;当生产者线程执行完同步代码块的时候,就会 释放同步锁,其他线程开始抢锁的使用权,抢到后就会拥有该同步锁,执行完成后释放,其他线程再开始抢锁的使用权,依次往复执行。
  2. 多个线程只有使用 同一个对象 (就好比案例中的共享资源对象)的时候,多线程之间才有互斥效果,我们把这个 用来做互斥的对象称之为同步监听对象,又称 同步监听器、互斥锁、同步锁,同步锁是一个抽象概念,可以理解为在对象上标记了一把锁。
  3. 同步锁对象可以选择 任意类型的对象 即可,只需要保证多个线程使用的是相同锁对象即可。在任何时候,最多只能运行一个线程拥有同步锁 。因为只有同步监听锁对象才能调用waitnotify方法,waitnotify 方法存在于 Object 类中。

线程通信之 wait 和 notify 方法

java.lang.Object 中提供了用于操作线程通信的方法,详情如下:

  • wait()执行该方法的线程对象会释放同步锁,然后 JVM 把该线程存放到 等待池 中,等待着其他线程来唤醒该线程;
  • notify()执行该方法的线程会 唤醒 在等待池中处于等待状态的的 任意一个线程 ,把线程转到 同步锁池 中等待;
  • notifyAll()执行该方法的线程会 唤醒 在等待池中 处于等待状态的所有的线程 ,把这些线程转到 同步锁池 中等待;

注意:上述方法只能被 同步监听锁对象 来调用,否则发生 IllegalMonitorStateException

wait 和 notify 方法应用实例

假设 A 线程 B 线程 共同操作一个 X 对象(同步锁),A、B 线程 可以通过 X 对象waitnotify 方法来进行通信,流程如下:

  1. A 线程 执行 X 对象 的同步方法时, A 线程 持有 X 对象 的锁, B 线程 没有执行机会,此时的 B 线程 会在 X 对象 的锁池中等待;
  2. A 线程 在同步方法中执行 X.wait() 方法时, A 线程 会释放 X 对象 的同步锁,然后进入 X 对象 的等待池中;
  3. 接着,在 X 对象 的锁池中等待锁的 B 线程 获取 X 对象 的锁,执行 X 的另一个同步方法;
  4. B 线程 在同步方法中执行 X.notify() 方法时,JVM会把 A 线程 X 对象 的等待池中转到 X 对象 的同步锁池中,等待获取锁的使用权;
  5. B 线程 执行完同步方法后,会释放拥有的锁,然后 A 线程 获得锁,继续执行同步方法;

基于上述机制,我们就可以使用 同步操作 + wait 和 notify 方法 来解决案例中的问题了,重新来实现共享资源——超市对象:

// 超市
public class Supermarket {

    // 蔬菜名称
    private String name;
    // 蔬菜数量
    private Integer num;
      // 超市是否为空
      private Boolean isEmpty = true;

    // 蔬菜基地向超市输送蔬菜
    public synchronized void push(String name, Integer num) {
          try {while (!isEmpty) {   // 超市有货时,不再输送蔬菜,而是要等待消费者获取
                   this.wait();}
                this.name = name;
                this.num = num;
              isEmpty = false;
              this.notify();                 // 唤醒另一个线程} catch(Exception e) {}}

    // 用户从超市中购买蔬菜
    public synchronized void popup() {
        
        try {while (isEmpty) { // 超市无货时,不再提供消费,而是要等待蔬菜基地输送
                   this.wait();}
              // 为了让效果更明显,在这里模拟网络延迟
            Thread.sleep(1000);
              System.out.println("蔬菜:" + this.name + "," + this.num + "颗。");
              isEmpty = true;
              this.notify();  // 唤醒另一线程} catch (Exception e) {}}
}

线程通信之 使用 Lock 和 Condition 接口

由于 waitnotify方法,只能被同步监听锁对象来调用,否则发生
IllegalMonitorStateException。从Java 5 开始,提供了 Lock 机制 ,同时还有 处理 Lock 机制 的通信控制的 Condition 接口Lock 机制 没有同步锁的概念,也就 没有自动获取锁和自动释放锁的这样的操作了。

因为没有同步锁,所以 Lock 机制 中的线程通信就不能调用 waitnotify方法了;同样的,Java 5 中也提供了解决方案,因此从 Java 5 开始,可以:

  1. 使用 Lock 机制 取代 synchronized 代码块synchronized 方法;
  2. 使用 Condition 接口 对象的 await、signal、signalAll 方法取代 Object 类中的 wait、notify、notifyAll 方法;

Lock 和 Condition 接口 的性能也比同步操作要高很多,所以这种方式也是我们推荐使用的方式。

我们可以使用 Lock 机制和 Condition 接口 方法来解决案例中的问题,重新来实现的共享资源——超市对象,代码如下:

// 超市
public class Supermarket {

    // 蔬菜名称
    private String name;
    // 蔬菜数量
    private Integer num;
      // 超市是否为空
      private Boolean isEmpty = true;
        // lock
        private final Lock lock = new ReentrantLock();
        // Condition
        private Condition condition = lock.newCondition();
        

    // 蔬菜基地向超市输送蔬菜
    public synchronized void push(String name, Integer num) {lock.lock(); // 获取锁
          try {while (!isEmpty) {   // 超市有货时,不再输送蔬菜,而是要等待消费者获取
                   condition.await();}
                this.name = name;
                this.num = num;
              isEmpty = false;
              condition.signalAll();} catch(Exception e) { } finally {lock.unlock();  // 释放锁
        }
        
    }

    // 用户从超市中购买蔬菜
    public synchronized void popup() {lock.lock();
        try {while (isEmpty) { // 超市无货时,不再提供消费,而是要等待蔬菜基地输送
                   condition.await();}
              // 为了让效果更明显,在这里模拟网络延迟
            Thread.sleep(1000);
              System.out.println("蔬菜:" + this.name + "," + this.num + "颗。");
              isEmpty = true;
              condition.signalAll();} catch (Exception e) { }   finally {lock.unlock();
        }
    }
}

完结,老夫虽不正经,但老夫一身的才华!关注我,获取更多编程科技知识。

正文完
 0