什么是Producer-Consumer Pattern?
设想一个场景,生产者需要将数据安全地交给消费者,然而生产者和消费者运行在不同的线程上,两者的处理速度差将是最大的问题。消费者想要取出数据的时候生产者还没有建立数据,或者生产者建立数据的时候消费者还没办法接受数据等等。
这个模型就是在他们中间加入一个桥梁,处理线程之间的处理速度差。
当两方都只有一个的时候,我们也称之为Pipe Pattern(管道模型)
同样的,我们还是用代码来做一个示例,设想现在有一个场景,有一个桌子,桌子上最多能放3块蛋糕,3个厨师往桌子上制作蛋糕,3个食客吃桌子上的蛋糕,当桌子上的蛋糕个数等于3个的时候,厨师就不能再放蛋糕了,当桌子上的蛋糕等于0个的时候,食客就不能吃蛋糕了。我们来实现一下:
首先写一个table类:
package producerConsumerPattern;
public class Table {
/*
* 原理上是循环队列
* */
private final String[] buffer;//存放蛋糕
private int tail;//尾巴
private int head;//头部
private int count;//buffer的蛋糕个数
public Table(int count){
this.buffer = new String[count];//最大能放的蛋糕个数
this.head = 0;
this.tail = 0;
this.count = 0;
//原理上是循环队列
}
//放置蛋糕 抛出的异常表示这个方法可被打断
public synchronized void put(String cake) throws InterruptedException{
while(count>=buffer.length){//警戒条件 当count<buffer.length的时候不能再放置
wait();
}
//进入临界区
buffer[tail] = cake;//放置在尾部
tail = (tail+1)%buffer.length;
count++;
System.out.println(Thread.currentThread().getName()+"puts "+cake+" ,还剩"+count+"块蛋糕");
notifyAll();//警戒条件可能发生了改变 对其他所有线程进行唤醒
}
//获取蛋糕
public synchronized String take() throws InterruptedException{
while(count<=0){//警戒条件 当count>0的时候才可以拿
wait();
}
String cake = buffer[head];//拿出头
head = (head + 1)%buffer.length;
count--;
System.out.println(Thread.currentThread().getName()+" takes "+cake+" ,还剩"+count+"块蛋糕");
notifyAll();//警戒条件可能发生了改变 对其他所有线程进行唤醒
return cake;
}
}
然后写食客线程:
package producerConsumerPattern;
import java.util.Random;
public class EaterThread implements Runnable {
private final Table table;
private final Random random;
public EaterThread(Table table , long seed){
this.table = table;
this.random = new Random(seed);
}
@Override
public void run() {
try{
while(true){
table.take();
Thread.sleep(random.nextInt(1000));//模拟吃蛋糕的间隔
}
}
catch(InterruptedException e){
}
}
}
实现厨师线程类:
package producerConsumerPattern;
import java.util.Random;
public class MakerThread implements Runnable {
/*
* 生产蛋糕的线程
* */
private final Random random;//随机产生生产蛋糕的时间
private final Table table;//放置的桌子
private static int id = 0;//蛋糕编号
public MakerThread(Table table,long seed){
this.table = table;
this.random = new Random(seed);
}
@Override
public void run() {
try{
while(true){
String cake = "No. "+nextId();
table.put(cake);
Thread.sleep(random.nextInt(1000));//随机休眠
}
}
catch(InterruptedException e){
e.printStackTrace();
}
}
private static synchronized int nextId(){
return id++;
}
}
最后写测试类来进行测试:
package producerConsumerPattern;
public class Test {
public static void main(String[] args) {
Table table = new Table(10);//可以放置3块蛋糕
EaterThread eaterThread = new EaterThread(table,System.currentTimeMillis());
MakerThread makerThread = new MakerThread(table,System.currentTimeMillis());
new Thread(eaterThread,"食客1").start();
new Thread(eaterThread,"食客2").start();
new Thread(eaterThread,"食客3").start();
new Thread(makerThread,"厨师1").start();
new Thread(makerThread,"厨师2").start();
new Thread(makerThread,"厨师3").start();
}
}
运行结果如下:
现在我们回头看一下代码,实际上和我们之前说的guarded suspention pattern很类似,在Table这个类中,对于put方法和take方法都存在他自己的警戒条件,满足警戒条件就进入临界区,否则就进入wait set。我们在这两个方法都抛出了InterruptedException,说明这个方法是可以取消的方法。
关键在于保护安全性的table类,这个类控制了生产者消费者的共享互斥,关于synchronized、wait、notifyAll这些考虑多线程操作的代码,全都隐藏在Table类里,这也是这个模式的关键。
Producer-Consumer Pattern的所有参与者
1、data参与者
data参与者由producer参与者建立,由consumer参与者所使用
2、producer参与者
producer参与者建立data参与者,传递给channel参与者
3、consumer参与者
consumer参与者从channel参与者获取data参与者
4、channel参与者
channel参与者从producer参与者接收data参与者,并且保管起来。根据consumer的要求,将data参与者传送出去,为了确保安全,producer和consumer应该和访问进行共享排斥。(将访问方法放在channel对象里,用synchronized关键字进行修饰可以有效排斥)这个类对于生产者消费者的访问关系调控起到了极其重要的作用。
如果没有这个channel参与者,消费者处理的时间也在生产者生产的时间内,这很不合理(设想一下厨师做完直接送到客户嘴边,等客户吃完然后再做另一个蛋糕,这不合理)。
InterruptedException异常
在示例代码中,我们提到了InterruptedException,这通常表示着:
1、这是一个需要花点时间的方法
2、这是一个可以取消的方法
三个常用的抛出InterruptedException的方法有
1、java.lang.Object类的wait方法
花费时间:进入等待区需要被notify/notifyAll,在等待的期间,线程不会活动,因此需要花费时间。
取消操作:使用notify/notifyAll方法取消
2、java.lang.Thread类的sleep方法
花费时间:会暂停执行参数内设置的时间
取消操作:等待设置长度时间
3、java.lang.Thread类的join方法
花费时间:会等待到指定的线程结束为止,也会花费直到指定线程结束之前这段时间。
取消操作:等待指定线程结束
取消线程sleep()暂停状态的interrupt方法
现在存在一个线程a,a调用了sleep方法:Thread.sleep(9999999999);进行休眠着,对于另一个线程b,可以执行下面的语句a.interrupt();使得a放弃等待操作。
在执行interrupt方法的时候,不需要获取Thread实例的锁,任何线程在任何时刻都可以调用其他线程的interrupt方法。当sleep中的线程被调用interrupt方法会抛出InterruptedException异常,比如上方a线程抛出异常,这样a的控制权,就交给捕捉这个异常的catch块了。
取消线程wait()等待状态的interrupt方法
当线程a在wait()等待的时候,也可以调用interrupt方法进行取消,表示不用等notify/notifyAll了,从等待区里直接出来,同样,a线程抛出InterruptedException异常。
但是当线程wait的时候,小心锁的问题,线程进入等待区的时候,会解除锁,当wait中的线程被调用interrupt的时候,会重新获取锁定,再抛出InterruptedException。获取锁之前,无法抛出这个异常。
notify方法与Interrupt方法
notify/notifyAll是Object类的方法,是该实例的等待区调用的,而不是对线程直接调用,notify/notifyAll方法所唤醒的线程会进入wait下一个语句。执行该方法需要获取锁。
interrupt方法是Thread的方法,对线程直接调用,当被interrupt的线程正在sleep或者wait的时候,会抛出InterruptedException异常。执行该方法不需要获取锁。
join方法和interrupt方法
当线程以join等待其他线程结束的时候,可以interrupt方法取消,和sleep时一样,会跳到catch模块。
Interrupt方法只是改变了中断状态
实际上这个方法只是改变了线程的中断状态(表示这个线程有没有被中断的状态)。不是一被调用interrupt方法就抛出InterruptedException。之所以有时候会抛出这个异常,是因为sleep、wait、join这些方法会不断检查中断状态的值,然后抛出InterruptedException,如果没有执行到这些方法,就不会抛出异常,而是一直进行自己的操作,直到执行到这些方法,才马上抛出InterruptedException。
总之,没有调用sleep、wait、join,那么InterruptedException时不会抛出的。
isInterrupted方法----检查中断状态
线程中断,返回true,没有中断,返回false。
Thread.interruptted方法----检查并且清除中断状态
检查当前线程是否中断,中断返回true并且设置为非中断状态,否则返回false。
interrupted方法和interrupt方法的区别:
interrupt方法将线程切换到中断状态
interrupted方法检查并且清除中断状态
不要使用Thread类的stop方法,很危险,因为就算线程在执行临界区间的内容,也会结束线程!