异步编程学习之路(三)-多线程之间的协作与通信

本文是异步编程学习之路(三)-多线程之间的协作与通信,若要关注前文,请点击传送门:

异步编程学习之路(二)-通过Synchronize实现线程安全的多线程

通过前文,我们学习到如何实现同步的多线程,但是在很多情况下,仅仅同步是不够的,还需要线程与线程协作(通信),生产者/消费者问题是一个经典的线程同步以及通信的案例。该问题描述了两个共享固定大小缓冲区的线程,即所谓的“生产者”和“消费者”在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。要解决该问题,就必须让生产者在缓冲区满时休眠(要么干脆就放弃数据),等到下次消费者消耗缓冲区中的数据的时候,生产者才能被唤醒,开始往缓冲区添加数据。同样,也可以让消费者在缓冲区空时进入休眠,等到生产者往缓冲区添加数据之后,再唤醒消费者,通常采用线程间通信的方法解决该问题。如果解决方法不够完善,则容易出现死锁的情况。出现死锁时,两个线程都会陷入休眠,等待对方唤醒自己。该问题也能被推广到多个生产者和消费者的情形。

假设有这样一种情况,有一个西餐厅,厨师负责制作牛排并放到顾客的盘子里,顾客负责享用盘子里的牛排,A线程可以看做厨师,B线程可以看做是顾客,如果盘子里面没有牛排,则B线程进入阻塞队列,此时唤醒A线程制作牛排,如果盘子里有牛排,则A线程进入阻塞队列,此时唤醒B线程享用牛排,如此循环有序的进行。代码如下:

/**
 * @Description:多线程之间的协作与通信
 * @Author:zhangzhixiang
 * @CreateDate:2018/12/21 12:53:36
 * @Version:1.0
 */
public class Plate {

    private List<Object> foods = new ArrayList<>();

    public synchronized void enjoy() {
        while (foods.size() == 0) {
            try {
                wait();
            } catch(InterruptedException e) {
                e.printStackTrace();
            }
        }
        Object food = foods.get(0);
        foods.clear();
        notify();//唤醒阻塞队列线程到就绪队列
        System.out.println(String.format("顾客正在享受%s,好吃点赞。", food));
    }

    public synchronized void cooking() {
        while (foods.size() > 0) {
            try {
                wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        Object food = "牛排";
        foods.add(food);
        notify();//唤醒阻塞队列线程到就绪队列
        System.out.println(String.format("厨师制作%s,并放到顾客的盘子里。", food));
    }

    public static void main(String[] args) {
        Plate plate = new Plate();
        for (int i = 0; i < 10; i++) {
            new Thread(() -> plate.cooking()).start();
            new Thread(() -> plate.enjoy()).start();
        }
    }

}

运行结果:

厨师制作牛排,并放到顾客的盘子里。
顾客正在享受牛排,好吃点赞。
厨师制作牛排,并放到顾客的盘子里。
顾客正在享受牛排,好吃点赞。
厨师制作牛排,并放到顾客的盘子里。
顾客正在享受牛排,好吃点赞。
厨师制作牛排,并放到顾客的盘子里。
顾客正在享受牛排,好吃点赞。
厨师制作牛排,并放到顾客的盘子里。
顾客正在享受牛排,好吃点赞。
厨师制作牛排,并放到顾客的盘子里。
顾客正在享受牛排,好吃点赞。
厨师制作牛排,并放到顾客的盘子里。
顾客正在享受牛排,好吃点赞。
厨师制作牛排,并放到顾客的盘子里。

以上代码有几点需要注意:

1、运行结果中只打印了8次,并且程序还处于运行状态?很明显这里出现了死锁,A和B两个线程都进入了休眠状态,等待对方唤醒自己。

这里的死锁有三种解决方案:

(1)给wait()设置最大等待时间(ms),超过最大等待时间后对应线程会自动消亡。

    wait(10);//最大等待时间10ms

(2)将notify()改为notifyAll(),notify()和notifyAll的区别,前一个是唤醒阻塞队列中的任意线程,后一个是唤醒就绪阻塞队列中的全部线程。(synchronize中有两个队列,一个是阻塞队列,一个是就绪队列)。

    notifyAll();//唤醒阻塞队列线程到就绪队列

(3)通过Jdk中JUC包下的ReentrantLock和Condition来替换这里的synchronize、wait、notify。(ReentrantLock和Condition在后文中会进行详细讲解)

2、Jdk5中notify()唤醒的是任意一个线程,这就说明有可能唤醒的是厨师线程,也有可能唤醒的是顾客线程,这就造成了一个唤醒的不明确性,你不知道唤醒的是什么线程。在JDK7中给出了解决方案,Jdk7中可以根据业务通过Condition建立单独的阻塞队列,就比如这里的厨师放到厨师的阻塞队列中,顾客放到顾客的阻塞队列中,这样的话你也就知道唤醒的到底是什么类型的线程了。

本文中提到了Jdk7中JUC包下的Lock锁相关的方法,Lock相关介绍在后文中会进行详细讲解。本文中提到了wait、notify、notifyAll等多线程之间协作通信的相关方法,下面的文章中我们会更详细的介绍这些方法。

异步编程学习之路(四)-睡眠、唤醒、让步、合并

猜你喜欢

转载自blog.csdn.net/qq_19734597/article/details/85221122