1. 写在前面
前面的内容都是“单个“线程的这那的事儿,但是在实际应用过程中,经常有多个线程一起配合使用的例子,那多个线程之间,怎么进行通信呢?这就是本文所讲的内容
本文只讲最常见的线程通信方式,旨在理解其基本含义,至于线程通信所有方式的汇总、线程通信的原理等,此处不记录。
2. 举个多线程需要通信的例子吧
比如海港码头有一个仓库,里面存放各种各样的货物。有两批工人,一批是专门搬货物放到仓库里,一批是专门从仓库里取货,两批人需要协作,相交的地方就是这个仓库。假设搬东西去仓库的那批工人叫做”生产者“,从仓库搬走货物的那批工人叫”消费者“。
当仓库还没满的时候,生产者和消费者可以自己做自己的,生产者可以往仓库里”生产“货物,与此同时消费者可以从仓库里”消费“货物。
当仓库已经满了的时候,生产者就不能再往仓库里“生产”货物了,此时必须通知消费者”消费“货物,才能给仓库腾出空间来,生产者继续生产。
当仓库已经没货,空空如也的时候,消费者就不能再“消费”货物了,此时必须通知生产者”生产“货物,消费者才能有货可”消费“。
生产者的那批工人,和消费者的那批工人,就是两个线程,在这个例子中,两个线程需要严密的通信配合,才能保证整个流程的正常运转。这就是非常典型的“生产者-消费者模式”,同时也是多线程需要通信的非常典型的例子。
3. 怎么通信
在上面的例子中,我们不难发现,当需要通信的时候,无非就是生产者跟消费者说:“你等一等,我生产一下,好了告诉你,你再继续消费。“消费者对生产者说:”你等一等,我消费一下,好了告诉你,你再继续生产。”核心就是两个层面,即
- 你等一等
- 生产/消费 好了告诉你
Java语言中,其实已经有这两个核心点的具体方法了,即wait和notify/notifyAll。
wait就是本线程释放掉锁,等待,直到被通知唤醒。notify就是随机唤醒一个正在wait当前对象的线程,并让被唤醒的线程拿到对象锁。notifyAll就是唤醒所有正在wait当前对象的线程,但是被唤醒的线程会再次去竞争对象锁。一次只有一个线程能拿到锁,所有其他没有拿到锁的线程会被阻塞。推荐使用。
4. wait和notify,怎么去解决实际案例
比如上面提到的生产者消费者问题,用wait和notify怎么去配合使用呢?
广为流传的有两种方式,一是管程法,二是信号量法。这两个方法的深层次的含义,其实目前还不必去深究,只需要知道这两种方式,怎么去解决问题就好了。也就是说,通过这两种方式,掌握wait和notify的用法即可。
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;
}
}
执行结果
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;
}
}
执行结果
这里就更倾向于,生产一个或者一批产品,然后改变信号(标志位),生产者不再生产,消费者开始消费,然后消费一个或者一批产品,然后改变信号(标志位),消费者不再消费,生产者继续生产,如此交替。
5. 总结
本文通过对线程通信的典型案例“生产者-消费者模式”的分析,解释了为什么需要线程通信,以及怎么做到线程通信。当然,因为是基础篇,所以原理性的东西,就不会在这出现啦~
6. 展望
截止本篇文章,多线程并发基础系列,也就更新完了,后续将会更新多线程并发进阶知识。基础篇里没有介绍到的原理,在进阶篇里都将会讲到。