今天想写的是多线程的另一个用法,线程通信,以及由其出现的一个经典案例:生产者消费者模式。
所谓线程通信,又叫线程交互,指的是多个线程在完成某些任务时,多个线程之间发生的通信,先举个简单的例子:
假设我们需要使用多线程打印0-100之间的数,要求的是有两个线程交替进行打印:
首先新建一个线程类
重写run方法并写入循环打印语句,由于需要反复打印,为了方便我们直接套上死循环:
i是一个共享数据,为了避免出现重复无序等线程安全问题,加上锁:
synchronized (this){
if(i<=100){
System.out.println(Thread.currentThread().getName()+":"+i++);
}
}
在test类中启动两个线程:
可以看出两个线程并没有交替打印,因为两个线程抢占CPU的使用权是我们无法干预的,为了达到交替打印的效果,我们需要用到线程通信。
关于线程通信,我们会用到几个方法:
wait():使当前线程进入等待状态,等待的同时释放锁资源
notify()/notifyall():唤醒一个/所有当前监视器(锁)等待状态的线程
可以看出这两个功能是可逆的,所以我们先得弄清楚何为等待状态,就拿上面的例子来说吧,假设第一次打印数字0,是第一个线程完成,那么打印数字1,就应该由第二个线程完成,这时为了防止线程一再次抢到CPU资源,我们让其等待一会,也就是在打印一次完成后调用wait()方法:
调用wait()方法也需要try-catch,和sleep方法一样,对异常不处理,此时假设线程1进入了等待状态,而线程2完成任务后,也进入了等待状态,那么没有线程在进行了,所以我们需要在线程1进入等待而线程2进入打印语句前,对线程1进行唤醒,这样形成一个反复等待-唤醒的操作,就可以实现交替打印的效果了:
运行效果:
在这里我们会发现一个现象,虽然我们对打印语句加上了锁,只能进入一个线程,而我们又调用了wait()方法将进入的线程变为等待状态,但实际效果是两个线程在交替工作,这也就说明了wait()方法还拥有解锁的功能,而值得我们注意的是,wait()以及notify()/notifyall()方法必须用在同步中。
这就是一个简单的线程通信,同样,该模式有了一个很经典的案例,叫做生产者消费者模式,比如有这样一个流程:
生产者(producer)将产品交给店员(clerk),而消费者(customer)负责消费产品,商店对产品的容纳有上限(比如:20件),如果生产者生产了超过20件产品,店员会通知生产者停一会,当店里有空闲时,再进行生产,而如果店里没有产品了,店员会通知消费者,等有产品时再来消费。
那么在这其中,可能会碰到几个问题,比如当生产者比消费者快时,消费者会漏掉一些产品未消费,而当消费者比生产者快时,明明生产不过来了,却还会显示同样的数据。就像上面的打印例子一样,当我们不使用线程通信时,经常会出现两个线程打印的次数不相同,甚至有时候一个线程做完了所有工作。放在项目中,那就会面临数据丢失问题。
我们使用线程通信完成该模型,分别新建test类、店员clerk类、消费者customer类以及生产者producer类,为了方便我们就写在一起。
首先是店员clerk类:
class Clerk{
private int product;
public Clerk(int product) {
this.product = product;
}
public Clerk() {
}
public int getProduct() {
return product;
}
public void setProduct(int product) {
this.product = product;
}
//添加货物
public synchronized void addProduct(){
if(product<20){
System.out.println("生产者生产了第"+ product++ +"件产品");
}else{
System.out.println("货物已满!");
}
}
//售货
public synchronized void saleProduct(){
if (product>0){
System.out.println("消费者消费了第"+ product-- +"件产品");
}
else {
System.out.println("缺货");
}
}
}
店员类属性有产品件数,以及添加产品以及售出产品的方法
接下来是生产者Producer类以及消费者Customer类:
//生产者
class Producer implements Runnable{
private Clerk clerk;
public Producer(Clerk clerk) {
this.clerk = clerk;
}
public Producer() {
}
public Clerk getClerk() {
return clerk;
}
public void setClerk(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
while (true){
clerk.addProduct();
}
}
}
//消费者
class Customer implements Runnable{
private Clerk clerk;
public Customer(Clerk clerk) {
this.clerk = clerk;
}
public Customer() {
}
public Clerk getClerk() {
return clerk;
}
public void setClerk(Clerk clerk) {
this.clerk = clerk;
}
@Override
public void run() {
while (true){
clerk.saleProduct();
}
}
}
两者实现Runnable接口,都拥有店员属性,不同的是一个不停地生产一个不停地消费,最后在测试类中分别启动两个线程,这里我们没有使用线程通信,看看它的实际效果:
public static void main(String[] args) {
Clerk clerk =new Clerk();
Producer producer=new Producer(clerk);
Customer customer=new Customer(clerk);
new Thread(producer).start();
new Thread(customer).start();
}
从这里可以看出,货物一直都满,但一直没有进行消费,这是因为生产者线程一直抢在消费者线程前面,显然是不符合实际的,于是我们使用线程通信,逻辑与上面的打印例子相似,在一个线程完成功能后进入等待,在一线程进入功能前唤醒处于等待状态的线程,即在添加产品以及售出产品的功能中,假如wait()以及notify()/notifyall()方法:
//添加货物
public synchronized void addProduct(){
if(product<20){
System.out.println("生产者生产了第"+ product++ +"件产品");
notifyAll();
}else{
System.out.println("货物已满!");
try {
wait();
} catch (InterruptedException e) {
}
}
}
//售货
public synchronized void saleProduct(){
if (product>0){
System.out.println("消费者消费了第"+ product-- +"件产品");
notifyAll();
}
else {
System.out.println("缺货");
try {
wait();
} catch (InterruptedException e) {
}
}
}
运行结果:
这样就完成了正常的生产与消费关系。