多线程并发基础(5)线程通信

102 阅读5分钟

1. 写在前面

前面的内容都是“单个“线程的这那的事儿,但是在实际应用过程中,经常有多个线程一起配合使用的例子,那多个线程之间,怎么进行通信呢?这就是本文所讲的内容

本文只讲最常见的线程通信方式,旨在理解其基本含义,至于线程通信所有方式的汇总、线程通信的原理等,此处不记录。

2. 举个多线程需要通信的例子吧

比如海港码头有一个仓库,里面存放各种各样的货物。有两批工人,一批是专门搬货物放到仓库里,一批是专门从仓库里取货,两批人需要协作,相交的地方就是这个仓库。假设搬东西去仓库的那批工人叫做”生产者“,从仓库搬走货物的那批工人叫”消费者“。

当仓库还没满的时候,生产者和消费者可以自己做自己的,生产者可以往仓库里”生产“货物,与此同时消费者可以从仓库里”消费“货物。

当仓库已经满了的时候,生产者就不能再往仓库里“生产”货物了,此时必须通知消费者”消费“货物,才能给仓库腾出空间来,生产者继续生产。

当仓库已经没货,空空如也的时候,消费者就不能再“消费”货物了,此时必须通知生产者”生产“货物,消费者才能有货可”消费“。

生产者的那批工人,和消费者的那批工人,就是两个线程,在这个例子中,两个线程需要严密的通信配合,才能保证整个流程的正常运转。这就是非常典型的“生产者-消费者模式”,同时也是多线程需要通信的非常典型的例子。

3. 怎么通信

在上面的例子中,我们不难发现,当需要通信的时候,无非就是生产者跟消费者说:“你等一等,我生产一下,好了告诉你,你再继续消费。“消费者对生产者说:”你等一等,我消费一下,好了告诉你,你再继续生产。”核心就是两个层面,即

  • 你等一等
  • 生产/消费 好了告诉你

Java语言中,其实已经有这两个核心点的具体方法了,即waitnotify/notifyAll

  • wait就是本线程释放掉锁,等待,直到被通知唤醒。
  • notify就是随机唤醒一个正在wait当前对象的线程,并让被唤醒的线程拿到对象锁。
  • notifyAll就是唤醒所有正在wait当前对象的线程,但是被唤醒的线程会再次去竞争对象锁。一次只有一个线程能拿到锁,所有其他没有拿到锁的线程会被阻塞。推荐使用。

4. waitnotify,怎么去解决实际案例

比如上面提到的生产者消费者问题,用waitnotify怎么去配合使用呢?

广为流传的有两种方式,一是管程法,二是信号量法。这两个方法的深层次的含义,其实目前还不必去深究,只需要知道这两种方式,怎么去解决问题就好了。也就是说,通过这两种方式,掌握waitnotify的用法即可。

4.1 管程法解决生产者消费者模式

直接看代码(在这里生产者生产Food,消费者消费Food,缓冲区就是所谓“仓库”,也就是生产者和消费者产生交集的地方)

public class Main {
    public static void main(String[] args) {
        Container container = new Container();
        
        Productor productor = new Productor(container);
        Consumer consumer = new Consumer(container);
        
        productor.start();
        consumer.start();
    }
}
// 生产者
class Productor extends Thread {
    Container container;
    
    Productor(Container container) {
        this.container = container;
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println("生产了编号为 " + i + " 的食物");
            container.produceOneFood(new Food(i));
        }
    }
}

// 消费者
class Consumer extends Thread{
    Container container;
    
    Consumer(Container container) {
        this.container = container;
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            System.out.println("消费了编号为 " + container.consumeOneFood().id + " 的食物");
        }
    }
}

// 产品(比如这里用"食物"表示产品)
class Food {
    int id; // 产品id
    Food(int id) {
        this.id = id;
    }
}

// 缓冲区
class Container {
    int maxFoodNum = 10; // 表示缓冲区,最多能存10件产品
    ArrayList<Food> foods = new ArrayList<>(); // 产品集合

    // 生产者生产产品
    public synchronized void produceOneFood(Food food) {
        // 先判断容器是否已满
        if (foods.size() == maxFoodNum) {
            // 容器(缓冲区)已经满了,生产者生产的产品不能放入缓冲区了,生产者线程需要释放锁,消费者线程需要拿到锁,然后进行消费
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        // 走到这,有2种情况
        // 1. 本来缓冲区的产品数目就不足10个,没有执行if里面的逻辑
        // 1. 缓冲区的产品数量一开始有10个,执行if里面的逻辑,等待了消费者消费产品,并且消费者调用notify唤醒了生产者
        foods.add(food);
        this.notifyAll(); // 唤醒消费者
    }

    // 消费者消费产品
    public synchronized Food consumeOneFood() {
        if (foods.size() == 0) {
            // 容器(缓冲区)为空,消费者无法消费商品,需要释放掉锁,
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        
        Food food = foods.remove(foods.size() - 1);
        
        this.notifyAll();
        return food;
    }
}

执行结果

image.png

4.2 信号量法解决

所谓信号量法/信号灯法,其实更多还是偏向用一个标志位flag来表示是否可以继续“消费”或者“生产”




public class Main {
    public static void main(String[] args) {
        Container container = new Container();

        Productor productor = new Productor(container);
        Consumer consumer = new Consumer(container);

        productor.start();
        consumer.start();
    }
}
// 生产者
class Productor extends Thread {
    Container container;

    Productor(Container container) {
        this.container = container;
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            container.produceOneFood(new Food(i));
        }
    }
}

// 消费者
class Consumer extends Thread{
    Container container;

    Consumer(Container container) {
        this.container = container;
    }

    @Override
    public void run() {
        for (int i = 0; i < 20; i++) {
            container.consumeOneFood();
        }
    }
}

// 产品(比如这里用"食物"表示产品)
class Food {
    int id; // 产品id
    Food(int id) {
        this.id = id;
    }
}

// 缓冲区
class Container {
    boolean isNeedConsumer = false; // 标志是否需要被消费
    Food food;

    // 生产者生产产品
    public synchronized void produceOneFood(Food food) {
        if (isNeedConsumer) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }

        System.out.println("生产了编号为 " + food.id + " 的食物");
        this.food = food;
        isNeedConsumer = !isNeedConsumer;
        
        this.notifyAll(); // 唤醒消费者
    }

    // 消费者消费产品
    public synchronized Food consumeOneFood() {
        if (!isNeedConsumer) {
            try {
                this.wait();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        Food consumedFood = new Food(food.id);
        System.out.println("消费了编号为 " + food.id + " 的食物");
        food = null;
        isNeedConsumer = !isNeedConsumer;

        this.notifyAll();
        return consumedFood;
    }
}

执行结果

image.png

这里就更倾向于,生产一个或者一批产品,然后改变信号(标志位),生产者不再生产,消费者开始消费,然后消费一个或者一批产品,然后改变信号(标志位),消费者不再消费,生产者继续生产,如此交替。

5. 总结

本文通过对线程通信的典型案例“生产者-消费者模式”的分析,解释了为什么需要线程通信,以及怎么做到线程通信。当然,因为是基础篇,所以原理性的东西,就不会在这出现啦~

6. 展望

截止本篇文章,多线程并发基础系列,也就更新完了,后续将会更新多线程并发进阶知识。基础篇里没有介绍到的原理,在进阶篇里都将会讲到。