所谓生产者,就是产生数据的线程,消费者,就是使用数据的线程。两个线程的速度差将成为最大的问题,而该模式就是缓冲两者的速度差。一般来说,生产者和消费者会有多个。
以下是典型的例子程序。假设一个场景,有一个桌子Table,有若干个厨师MakerThread往桌子上按顺序放上蛋糕,有若干个顾客EaterThread按顺序吃蛋糕:
首先是厨师MakerThread:
public class MakerThread extends Thread{
private final Random random;
private final Table table;
private static int id = 0;
public MakerThread(String name, Table table,long seed) {
super(name);
this.random = new Random(seed);
this.table = table;
}
@Override
public void run() {
while (true){
try {
Thread.sleep(random.nextInt(1000));
String cake = "Cake.No" + nextId() + "by" + getName();
table.put(cake);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
private static synchronized int nextId(){
return id++;
}
}
很简单,不断将cake放入table中。
public class EaterThread extends Thread{
private final Random random;
private final Table table;
private static int id = 0;
public EaterThread(String name, Table table,long seed) {
super(name);
this.random = new Random(seed);
this.table = table;
}
@Override
public void run() {
while (true){
try {
String cake = table.take();
Thread.sleep(random.nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
很简单
,在循环中不断从table中取出蛋糕,并停留一段随机长的时间。接下来看看最重要的Table:
public class Table {
private final String[] buffer;
//下一个要放的位置
private int tail;
//下一个要取的位置
private int head;
//蛋糕数量
private int count;
public Table(int count) {
buffer = new String[count];
this.count = 0;
tail = 0;
head = 0;
count = 0;
}
public synchronized void put(String cake) throws InterruptedException{
while (count >= buffer.length){
wait();
}
System.out.println(Thread.currentThread().getName() + "put" + cake);
buffer[tail] = cake;
tail = (tail+1)%buffer.length;
count++;
notifyAll();
}
public synchronized String take() throws InterruptedException{
while (count <= 0){
wait();
}
String cake = buffer[head];
head = (head+1)%buffer.length;
count--;
notifyAll();
System.out.println(Thread.currentThread().getName() + "take" + cake);
return cake;
}
}
首先使用String数组作为底层的容器存放cake,放入和取出都是一个方向的,类似单向队列。注意到put和take方法,put的时候,先判断蛋糕数量是否为满,若为满,则
稍等,也就是当厨师将做好的蛋糕准备放到桌子上的时候发现桌子上没有位置可以放,则要等待,等到有顾客将蛋糕取走腾出位置,才可以将蛋糕放上去。同理,take的时候,要
先判断下蛋糕数是否为0,为0则等待厨师送上蛋糕(关于线程等待这部分,详细可见另一篇博文 java多线程设计模式之Guarded Suspension)。
最后是Main:
public class Main {
public static void main(String[] args){
Table table = new Table(10);
new MakerThread("maker-1",table,32912).start();
new MakerThread("maker-2",table,12345).start();
new MakerThread("maker-3",table,32234).start();
new EaterThread("eater-1",table,23456).start();
new EaterThread("eater-2",table,32456).start();
new EaterThread("eater-3",table,56765).start();
}
}
结果
就这么有条不紊地将做蛋糕和吃蛋糕进行下去。。
maker-2putCake.No9bymaker-2
maker-3putCake.No10bymaker-3
maker-3putCake.No11bymaker-3
maker-2putCake.No12bymaker-2
eater-2takeCake.No8bymaker-2
eater-1takeCake.No9bymaker-2
maker-1putCake.No13bymaker-1
maker-3putCake.No14bymaker-3
maker-2putCake.No15bymaker-2
这样子有什么好处呢?可以把上面的例子抽象为一张图:
看了图容易理解一些:
1.将产生数据和使用数据两个流程分离解耦,让二者不互相依赖以至于相互影响,大家都靠缓冲区进行交互,这是一种好的设计方式。
就比如顾客吃蛋糕如果每次都要找到厨师拿,如果厨师换人了顾客就不认识了拿不到蛋糕了。
2.由于生产者与消费者是两个独立的并发体,他们之间是用缓冲区作为桥梁连接,生产者只需要往缓冲区里丢数据,就可以继续生产下一个数据,而消费者只
需要从缓冲区了拿数据即可,这样就不会因为彼此的处理速度而发生阻塞。
就比如顾客吃的速度比厨师做得快,这样顾客要吃蛋糕只能一直等待厨师而不能做其他事。同理,当厨师做蛋糕速度快过顾客吃的时候,顾客还没吃完,
厨师就必须等顾客有人吃完才可以把蛋糕给他再回去做蛋糕。
最后,谈下使用线程方法interrupt和InterruptException如何优雅结束线程。
首先,interrupt方法是线程的实例方法,即线程对象调用之。当线程对象调用了该方法后,本质是对该线程对象的中断标志位进行置位操作。线程在进入sleep、join、wait这三个方法后会不断检查中断标志位,一旦中断标志位被置位,则抛出InterruptException,于是会退出死循环进入catch语句,也就是退出了线程(一般情况下不会在catch中死循环吧?)。
对于sleep、join来说,只要检测到中断标志位被置位就立刻抛出InterruptException,而wait由于此时线程已经交出了锁,所以要重新获取到锁之后才可以抛出异常。
另外,线程的实例方法isInterrupted是检测线程中断标志位是否被置位的方法,interrupted则是检查中断标志位后将标志位清空(如果已经被置位)。