先前我们讲解了多线程互斥访问同一资源问题,接下来我们讲解线程间通信问题之经典:生产者与消费者
线程通信:不同的线程在执行不同的任务,如果这些任务的执行之间存在先后顺序,那么线程之间必须要能够通信,协调的完成工作。
(一)经典案例:
(1)生产者和消费者等多个线程在访问共同资源时, 必须要互斥(即要访问的临界资源一样),只有拥有互斥对象的线程才有访问公共资源的权限,因为互斥对象只有一个,所以能保证公共资源不会同时被多个线程访问
(2)问题:为什么生产者不直接将生产好的产品直接交给消费者消费呢?
在这里体现了面向对象的设计理念:低耦合;
a)高耦合:生产者直接将生产好的产品交给消费者,那么生产者中药封装一个消费者对象的引用;消费者要消费生产者的产品,那么消费者就要封装一个生产者对象的引用,例如:主板和集成显卡;
b)低耦合:使用一个中间对象,屏蔽掉生产者和消费者之间直接数据交互的过程,例如:主板和独立显卡。
(二)线程通信之wait()和notify()、notifyAll()方法详解:
(1)先了解两个概念:
a)线程等待池(没有cpu资源,没有同步资源的监听器(同步锁),连获取同步锁的权利都没有,此线程就没有执行的可能);
b)锁池(拥有cpu资源,但是没有同步锁,但有获取同步锁的权利,多线程之间一旦获取到锁,就会立即执行);
(2)java.lang.Object类中提供了几个方法用于操作多线程之间的通信问题:
wait():执行该方法的线程对象释放同步锁,JVM把该线程存放在共享资源的等待池中,等待其他线程唤醒;
notify():执行该方法的线程,将唤醒共享资源等待池中任意、随机的一个线程。将其线程转移到锁池中;
notifyAll():执行该方法的线程将唤醒共享资源等待池中所有的线程,将其线程转移到锁池中。
注解:共享资源即为同步锁,也称为同步监听对象。
注意:
1)上述的三个方法只能被同步监听对象调用(即就是只能放在synchronized关键字修饰的同步代码块/同步方法中共享资源对象(this)来调用),否则报错illegalMonitorStateException异常;
2)多个线程只有共享同一资源,多线程之间才有互斥现象,我们将其共享资源称为:同步锁/同步监听对象/同步监听器;
3)因为只有同步监听对象才可以调用wait(),notify()以及notifyAll()方法,所以这些方法存在于Object类中,而不在Thread类中(只用让同步监听对象调用,才可以知道要释放的是哪个锁,唤醒的是哪个需要当前同步锁的线程对象)。
(三)生产者与消费代码:
生产者代码:
/**
*
* @author super 生产者用于生产数据
*/
public class Producer implements Runnable {
// 共享的资源对象(同一个资源对象)
private ShareResource resource = null;
public Producer(ShareResource resource) {
super();
this.resource = resource;
}
@Override
public void run() {
for (int i = 0; i < 50; i++) {
//生产操作
if (i % 2 == 0) {
resource.push("Jack", "男");
} else {
resource.push("Tom", "女");
}
}
}
}
消费者代码:
/**
*
* @author super
* 消费者
*/
public class Consumer implements Runnable{
// 共享的资源对象(同一个资源对象)
private ShareResource resource = null;
public Consumer(ShareResource resource) {
super();
this.resource = resource;
}
@Override
public void run() {
for (int i = 0; i < 50; i++) {
//消费操作
resource.popup();
}
}
}
共享资源(同步锁代码):
/**
*
* 共享资源对象(姓名-性别)
*/
public class ShareResource {
private String name;
private String gender;
//表示共享资源为空的状态(true为空就生产,fasle不为空就消费)
private boolean isEmpty = true;
/*
* 用于生产者向共享资源对象中存储数据
*/
public synchronized void push(String name,String gender){
try {
while(!isEmpty){ //不为空,等待消费者来消费
//释放同步锁,进入resource资源的等待池中,只能被其他线程所唤醒
this.wait();
}
//------生产开始------
this.name = name;
Thread.sleep(10);
this.gender = gender;
//------生产结束------
isEmpty = false; //设置共享资源中的数据不为空
//生产者生产完毕,就必须唤醒一个消费者,否则出现阻塞状态,全部都进入等待状态
this.notify();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
/*
* 用于消费者从共享资源对象中取出数据
*/
public synchronized void popup(){
try {
while(isEmpty){ //isEmpty = true时,共享资源为空,等待生产者进行生产
//释放同步锁,进入resource资源的等待池中,只能被其他线程所唤醒
this.wait();
}
//------消费开始------
Thread.sleep(10);
System.out.println(this.name + "-" + this.gender);
//------消费结束------
isEmpty = true; //设置共享资源中的数据为空
//消费者消费完毕,就必须唤醒一个生产者,否则出现阻塞状态,全部都进入等待状态
this.notify();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
测试代码:
/**
*
* @author lenovo
* 测试代码
*/
public class Test {
public static void main(String[] args) {
//创建生产者和消费者共同的资源对象
ShareResource resource = new ShareResource();
//启动生产者线程
new Thread(new Producer(resource)).start();
//启动消费者线程
new Thread(new Consumer(resource)).start();
}
}
重中之重:如果你使用了多个生产者,或者多个消费者线程,一定要在共享资源中每一处使用了notify()方法,唤醒在其同步监听对象的全部线程(即使用notifyAll()方法,而不是notify()方法),一定要切记,否者就会出现多个线程全部等待,无一唤醒,程序就死了!