[多线程] - 线程间的通信(wait及notify方法的应用)

一、前言

1. 什么是线程间的通信

我想看到标题很多人想到的第一反应大概是疑问什么是线程间的通信?其实这个概念很好理解,在我们的实际的业务开发中,很多场景都是多个线程之间配合进行工作的,就好比一条工厂的流水线的工人,每个工人之间都有分工,其中一部分工人负责生产零件然后将零件传递给下一部分的工人再进行加工,直到生产完完整的商品。线程之间在很多业务场景下也是如此,某条线程负责生产业务需求然后再分发给其他线程进项处理,无疑在多条线程配合的情况下,多线程之间需要进行有效的沟通才可以提高工作的效率,那么线程之间沟通的方式其实就是线程间的通信。

2. 提供一个简单的业务场景

俗话说包治百病,大多数女生对于男朋友送个包包都是不会拒绝的,尤其是当你送的包包是个限量的定制款的时候,你的女朋友并不会吝啬送给你一个香吻,那么我们就以定制款的包包做个需求提供方和需求处理方的业务场景模型:

  1. 需求提供方Producer(买方):负责提出定制包包的需求。
  2. 需求处理方式Consumer(卖方):包包厂商,负责生产包包。
    这里注意: 由于此时卖方还是个小厂商,一次仅能处理一条定制请求,所以在他的官网每次仅开放一个包包的定制需求。商户每日做多制作10个包包。

好了,我们先创建买方的Demo:

public class BagsProducer {
    
    
	// 创建一个集合来装定制请求
	private List<String> list;

	// 创建消费方法 msg为定制需求
	public void productMsg(String msg) {
    
    
		list.add(msg);
		System.out.println("您需求为: " + msg + "已经开始定制,请耐心等待,接单时间"
				+ new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date()).toString());
	}

	// 提供有参构造
	public BagsProducer(List<String> list) {
    
    
		super();
		this.list = list;
	}
}

然后在提供一个厂商的demo:

public class BagsConsumer {
    
    
	// 创建一个集合来装定制请求
	private List<String> list;
	// 每天开放定制数
	private final static int MAX = 10;
	// 包包编号
	private int count = 1;

	public void msgConsuption() {
    
    
		try {
    
    
			// 如果list大于0 标识此时有定制需求
			while (true) {
    
    
				if (count > MAX) {
    
    
					System.err.println("今日商铺停止接单!");
					return;
				}
				if (list.size() > 0) {
    
    
					String string = list.get(0);
					System.out.println("需求为: " + string + "的包包已经开始制作,开始制作时间为:"
							+ new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date()).toString());
					//计数
					System.err.println("计数器状态:"+ count++);
					list.remove(0);
					// 通过线程休眠 模拟包包制作过程
					Thread.sleep(1_000);
					System.out.println("需求为: " + string + "的包包已经制作完成,完成时间为:"
							+ new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date()).toString());
				}
			}
		} catch (InterruptedException e) {
    
    
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
	// 提供有参构造
	public BagsConsumer(List<String> list) {
    
    
		super();
		this.list = list;
	}
}

最后我们提供一个启动类:

public class ClassRunner {
    
    

	public static void main(String[] args) {
    
    
		ArrayList<String> list = new ArrayList<String>();
		BagsProducer bagsProducer = new BagsProducer(list);
		BagsConsumer bagsConsumer = new BagsConsumer(list);
		// 启动一号线程负责提供定制需求
		new Thread(() -> {
    
    
			for (int i = 0; i < 10; i++) {
    
    
				bagsProducer.productMsg("贵就完事了");
			}
		},"消费者线程").start();
		new Thread(() -> {
    
    
		bagsConsumer.msgConsuption();
		},"厂家线程").start();
	}
}

运行main方法,运行结果如下(这里由于程序存在BUG,需要在运行结束后手动退出线程!):
在这里插入图片描述

观察运行结果,我们发现这中间有很多的问题,首先我们看这里:
在这里插入图片描述

在这里我们是通过一个for循环调用的bagsProducer.productMsg方法,由于这里我们无法感知到bagsConsumer.msgConsuption是否已经将上一条请求信息处理结束,因此一次性的将所有请求数据都交给bagsProducer对象处理后失败,造成商家无法接到后续订单,因此我们对需求作如下优化:

  1. 用户提交定制需求,首先判断此时商家是否符合接单状态,不符合则用户需要等待商家可以接单后才可以再次下单
  2. 用户如下单成功,此时用户的预订接口应该为暂时不可用状态
  3. 商家接单后.处理定制需求,处理成功后将接单状态修改为可以接单

在这里我们发现通过之前的技术栈来实现暂停接单的业务无疑是有点复杂的,好在JDK已经为我们提供好了配套使用的API来完成生产/消费模型

二、 wait及notify的使用

1. wait方法的API简介

首先我们可以打开JDK提供的API文档,先查看下wait方法的描述:
在这里插入图片描述
其中标红的地方我们需要着重的讲一下

  1. 根据API描述我们可以看出当我们调用wait方法后,当前线程会进入等待状态(WAITING),一直到其他方法调用notify或者notifyAll,线程才会重新变为就绪态(RUNNABLE),也就是说明如果直接调用wait方法并不设置等待时间的话,该线程无法主动地唤醒自己,需要由其他线程调用方法进行唤醒。
  2. 这里图中的第二点和第三点其实讲的就是同一个事情,与sleep方法不同只能被Thread类调用不同,wait方法可以被任意Object对象调用,但是这里我们需要注意的是,调用wait方法的对象需要持有monitor对象,可以简单地理解为锁 (文档中的监视器是monitor的中文翻译,monitor与锁相似但是存在部分差异)。
  3. 当我们调用wait方法的时候,此时当前线程会释放所持有的锁

针对以上三点我们先做一个实验

  1. 为了证明上述第二点,我们先直接的调用wait方法,观察现象:
	// 修改下消费者的productMsg方法 添加wait
public class BagsProducer {
    
    
	// 创建一个集合来装定制请求
	private List<String> list;

	// 创建消费方法 msg为定制需求
	public void productMsg(String msg) {
    
    
		try {
    
    
			System.out.println("请求接单");
			if (list.size() > 0) {
    
    
				list.wait();
				return;
			}
			list.add(msg);
			System.out.println("您需求为: " + msg + "已经开始定制,请耐心等待,接单时间"
					+ new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date()).toString());
		} catch (InterruptedException e) {
    
    
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	// 提供有参构造
	public BagsProducer(List<String> list) {
    
    
		super();
		this.list = list;
	}
}

启动ClassRunner,观察结果:
在这里插入图片描述

这里看到消费者线程抛出了IllegalMonitorStateException异常,API文档中对该异常的描述为:
在这里插入图片描述
这里我们证明了调用wait方法需要持有锁才可以,解决这个问题我们只需要通过synchronized同步代码快为当前线程提供一个锁:

public class BagsProducer {
    
    
	// 创建一个集合来装定制请求
	private List<String> list;

	// 创建消费方法 msg为定制需求
	public void productMsg(String msg) {
    
    
		try {
    
    	
			System.out.println("请求接单");
			if (list.size() > 0) {
    
    
				synchronized (list) {
    
    
					list.wait();
					//这里删除了return
				}
			}
			list.add(msg);
			System.out.println("您需求为: " + msg + "已经开始定制,请耐心等待,接单时间"
					+ new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date()).toString());
		} catch (InterruptedException e) {
    
    
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	// 提供有参构造
	public BagsProducer(List<String> list) {
    
    
		super();
		this.list = list;
	}
}

这里我们通过CMD命令打开DOM窗口,然后输入jps查找线程号,然后通过jstack + 线程号 的方式查看线程状态:
在这里插入图片描述
此时消费者线程已经进入休眠,完成了我们预期的对于消费者线程的修改,接下来我们就要继续修改商家线程来配合wait方法进行业务优化。

2. notify方法的API简介

我们还是按照之前的习惯,先查看下API文档对于notify方法的描述:
在这里插入图片描述
对于红线处的理解我们可以概括为如下几点:

  1. notify方法主要用来唤醒同一个monitor对象下的wait状态的线程,这里需要注意的是,调用notify方法只会唤醒当前monitor下的一个线程而不是全部,如果有多个所属当前monitor对象的线程处于wait状态,只会随机唤醒其中一条而不是全部。
  2. 被唤醒的线程状态为就绪态(runnable),此时被唤醒的线程需要与其他线程重新争夺锁
  3. notify可以被任意Object对象调用,调用的前提是需要持有monitor对象(监视器)
  4. 调用notify对象不会立即释放锁,这点需要注意

这里我们开始进行BagsConsumer对象的改造:

public class BagsConsumer {
    
    
	// 创建一个集合来装定制请求
	private List<String> list;
	// 每天开放定制数
	private final static int MAX = 10;
	// 包包编号
	private int count = 1;

	public void msgConsuption() {
    
    
		try {
    
    
			// 如果list大于0 标识此时有定制需求
			while (true) {
    
    
				if (count > MAX) {
    
    
					System.err.println("今日商铺停止接单!");
					return;
				}
				if (list.size() > 0) {
    
    
					String string = list.get(0);
					System.out.println("需求为: " + string + "的包包已经开始制作,开始制作时间为:"
							+ new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date()).toString());
					//计数
					System.err.println("计数器状态:"+ count++);
					list.remove(0);
					// 通过线程休眠 模拟包包制作过程
					Thread.sleep(1_000);
					System.out.println("需求为: " + string + "的包包已经制作完成,完成时间为:"
							+ new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date()).toString());
					synchronized (list) {
    
    
						list.notify();
					}
				}
			}
		} catch (InterruptedException e) {
    
    
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	// 提供有参构造
	public BagsConsumer(List<String> list) {
    
    
		super();
		this.list = list;
	}
}

运行结果:
在这里插入图片描述

这里看到程序已经按照我们的预期实现了业务处理。但是显然我们不会满足于这么简单的也无需求,毕竟商家也会做强做大的是不是,那么我们就要提出新的业务模型来引出我们下面的知识点。

3. 新的业务需求

过了段时间,越来越多的商家发现商机,开始制作定制包包,而每个包包厂家由于能够接到的单子有限,为了节约成本,商家在没有单子的时候会给工人放假,直到系统派单为止,我们来总结下需求:

  1. 商户端:现在包包的定制商分为LV,Hermes(爱马仕),Chanel(香奈儿)三家,每家每日的接单上限依旧为10个,为了提高定制质量,同一时间同一商家只能接一个订单,为了节约人力成本,员工需要等到用户下单后才需要上班。
  2. 用户端:用户现在下单需要做两个判断,第一点用户依旧每次只能下一个单,需要等待上一个订单完成用户倒卖后才能有钱下下个订单,用户需要等待商家空闲后才可下单,每次下单后需要通知一个商家开工。

这里我们先对新的需求进行实现,观察是否有新的问题发生
用户端:

public class Client {
    
    

	// 创建一个集合来装定制请求 此时集合允许最多存放三个请求
	private List<String> list;

	// 创建消费方法 msg为定制需求
	public void productMsg(String msg) {
    
    
		try {
    
    
			// 这里要判断list的size是否大于1 当size大于1的时候代表此时三个商家都已经接单处于制作中,按照需求此时用户不允许下单
			if (list.size() > 0) {
    
    
				synchronized (list) {
    
    
					list.wait();
				}
			}
			// 用户下单
			list.add(msg);
			System.out.println();
			System.out.println("需求为" + msg + "的包包已下单,订单创建时间为:"
					+ new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date()).toString());
			// 下单后用户需要通知一个商家上班
			synchronized (list) {
    
    
				list.notify();
			}
		} catch (InterruptedException e) {
    
    
			// TODO Auto-generated catch block
			e.printStackTrace();
		}

	}

	public Client(List<String> list) {
    
    
		super();
		this.list = list;
	}
}

商家端

public class Business {
    
    

	// 创建一个集合来装定制请求
	private List<String> list;
	// 创建每日最大接单
	private final static int MAX = 10;
	// 创建计数器
	private int count = 1;

	public void make() {
    
    
		try {
    
    
			while (true) {
    
    
				String demand;
				synchronized (list) {
    
    
					if (count > 10) {
    
    
						System.err.println(Thread.currentThread().getName() + "商户停止接单");
						System.out.println();
						// 商户下班
						return;
					} 
					// 没有需求的时候 商户休息
					if (list.size() == 0) {
    
    
						list.wait();
						continue;
					}
					count++;
					demand = list.remove(0);
					System.out.println(Thread.currentThread().getName() + "已接单需求为" + demand + "的包包,接单时间为:"
							+ new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date()).toString());
				}
				// 通过休眠 模拟制作过程
				Thread.sleep(1_000);
				System.out.println("需求为" + demand + "的包包已制作完成,完成时间为:"
						+ new SimpleDateFormat("yyyy-MM-dd hh:mm:ss").format(new Date()).toString());
				// 此时通知客户可以继续下单
				synchronized (list) {
    
    
					list.notify();
				}
			}
		} catch (InterruptedException e) {
    
    
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}

	// 有参构造
	public Business(List<String> list) {
    
    
		super();
		this.list = list;
	}

}

启动类:

public class ClassRunner {
    
    

	public static void main(String[] args) {
    
    
		List<String> list = new ArrayList<String>();
		new Thread(() -> {
    
    
			Client client = new Client(list);
			for (int i = 1; i < 31; i++) {
    
    
				client.productMsg("圣诞节限定款编号为" + i);
			}
		}, "买家线程").start();
		try {
    
    
			// 通过休眠 模拟下单延时时间
			Thread.sleep(1_000);
		} catch (InterruptedException e) {
    
    
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		//创建三个商家
		Stream.of("LV", "Hermes","Chanel").forEach(s -> {
    
    
			new Thread(() -> {
    
    
				Business business = new Business(list);
					business.make();
			}, s).start();
		});

	}
}

启动观察结果:
在这里插入图片描述
这里可以看到,用户在下了两次单后就停止了下单,明显不符合我们的业务逻辑,这是为什么呢?

4. 线程的假死

我们通过jstack的方式观察下此时线程的状态:
在这里插入图片描述
这里我们发现我们的买家线程和三个卖家线程全部进入到了wait状态,造成这个现象的主要原因是因为notify方法的特性为随机唤醒一条wait线程,于是就出现了下面的业务场景:

  1. 用户提供了一个定制需求后调用notify方法唤醒一个商家制作包包。
  2. 用户线程重新进入判断,由于此时有未完成定制,用户线程进入wait状态
  3. 三个商家之一接到notify指令唤醒变为runnable状态,开始执行用户定制需求,处理结束后,用户调用notify方法随机唤醒一个等待线程。
  4. 此时另外一个wait商家接到唤醒指令,装换为runnable状态,但是此时并没有用户提供新的需求,继而转为wait状态
  5. 至此四个线程全部进入wait状态,出现假死现象

5. notifyAll

解决上述的假死状态很简单,将商家端(Business对象)的notify方法更换为notifyAll即可。

notify和notifyAll的区别:

  1. notify方法随机唤醒一条monitor对象下的wait线程
  2. notifyAll方法唤醒全部属于此monitor对象下的wait线程

更换后运行结果如下:
在这里插入图片描述
至此应用多线程通信的一对多的消费模型简单实现就完成了

三、补充

1. wait和notify方法的锁状态

  1. 当线程调用wait方法后会立即释放锁
  2. 当线程调用notify方法后会等当前线程全部业务代码执行完毕后才会释放锁

2. wait和sleep的区别

1.所属调用方不同
sleep只能由Thread来调用,而wait可以被所有Object对象调用。
2.锁状态不同
wait方法会立即释放锁,而sleep会继续持有锁知道所有业务逻辑处理结束。
3.是否需要monitor
wait方法需要当前线程持有monitor,sleep则不需要。

至此,今天要讲的wait和notify的应用就全部结束了,如果代码中存在错误或不清楚的地方欢迎指正,如果觉得看了这篇文章有收获的同学,希望可以点个赞加个关注~
祝好!

猜你喜欢

转载自blog.csdn.net/xiaoai1994/article/details/110556416