Producer是生产者的意思,指的是生成数据的线程,Consumer则是消费者的意思,指的是使用数据的线程,这里我们要实现的是如何安全的将数据从生产者交给消费者,看似简单但是当两者处于不同的线程时,处理速度差异便会产生问题。对于这个问题,我们可以在两者之间加入一个桥梁通道的角色,具体的设计如下:
情景创建,假如说有个蛋糕店,糕点师制作蛋糕,顾客吃蛋糕,糕点师制作完成后将蛋糕放在桌子上,桌子上做多可以放置三个蛋糕,如果桌子上满了就必须等到腾出位置,客人如果吃没了就要等待蛋糕重新放置到桌子上。
首先构建用于存放蛋糕的Table类:
public class Table {
private final String[] buffer;
private int tail;
private int head;
private int count;
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{
System.out.println(Thread.currentThread().getName()+ " puts "+cake);
//守护条件,当条件成立时,执行wait()
while(count>=buffer.length){
wait();
}
buffer[tail]=cake;
tail=(tail+1)%buffer.length;
count++;
notifyAll();
}
//拿取蛋糕
public synchronized String take()throws InterruptedException{
//守护条件,当条件成立时,执行wait()
while(count<=0){
wait();
}
String cake = buffer[head];
head = (head+1)%buffer.length;
count--;
//notifyAll()方法调用之后会将原来wait()方法等待的线程唤醒
//继续执行noifyAll()方法后面的语句,执行notifyAll方法时必须获取对应的实例的锁
notifyAll();
System.out.println(Thread.currentThread().getName()+" takes "+cake);
return cake;
}
}
其中对cake操作的方法:get和put都是用synchronized修饰,保证线程的安全性,同时通过构造函数来传递所要构建的Table 的容量。其中也是用到了之前将讲到的GuardSuspension模式,设置while语句对队列中的内容的判断作为守护条件。
用于表示蛋糕制作的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.table=table;
this.random=new Random(seed);
}
public void run(){
try{
while(true){
Thread.sleep(random.nextInt(1000));
String cake ="[ cake No."+nextId() + " by "+getName() + "]";
table2.put(cake);
}
}catch(InterruptedException e){
}
}
//保证修改公用字段id在多个线程修改的时候能够保持互斥性
private static synchronized int nextId(){
return id++;
}
}
此类适用于向Table中添加蛋糕的,同时,再run()方法中设置sleep方法来模拟写入的耗时过程;类的初始化需要传入一个共享的Table类的实例,所以Table类要设计为线程安全的类才可以。这样就实现了往Table中添加cake的过程。
下面模拟客人拿取蛋糕的过程,构建了一个EaterThread类:
public class EaterThread extends Thread {
private final Table table;
private final Random random;
public EaterThread(String name ,Table table,long seed){
super(name);
this.table = table;
this.random = new Random(seed);
}
public void run(){
try{
while(true){
String cake = table2.take();
Thread.sleep(random.nextInt(1000));
}
}catch(InterruptedException e){
}
}
}
这个类模拟了对蛋糕的获取的行为,同样的类的初始化需要传入一个共享的Table类的实例,这就看出了Table类的安全性对整个的模拟过程的重要性,Table类起到了一个桥梁的角色,而且含包含了对数据的操作的方法,所以重点则是对于Table类的设计。
下面编写一个main函数来测试一下:
public class ProducerConsumerMain {
public static void main(String[] args) {
Table2 table2 = new Table2(3);
new MakerThread("MakerThread-1",table2,31415).start();
new MakerThread("MakerThread-2",table2,92653).start();
new MakerThread("MakerThread-3",table2,58979).start();
new EaterThread("EaterThread-1",table2,32384).start();
new EaterThread("EaterThread-2",table2,62643).start();
new EaterThread("EaterThread-3",table2,38327).start();
}
}
模拟了3个糕点师和3客人的情况,所以各启动了3个线程,执行结果如下:
关于Channel角色存在的意义,因为正因为有了Channel角色的存在才使得整个过程能够协调有序的进行,所以在这里我们应该这么考虑:线程的协调运行系要考虑放在中间的东西,同时应该注意应该保护的东西。