知其然还要知其所以然,为了解决什么问题才构建了这一模型?什么情况下使用该模型?
关于生产者消费者问题:
生产者消费者问题(英语:Producer-consumer problem),也称有限缓冲问题(英语:Bounded-buffer problem),是一个多线程同步问题的经典案例。该问题描述了共享固定大小缓冲区的两个线程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。
.
要解决该问题,就必须让生产者在缓冲区满时休眠(要么干脆就放弃数据),等到下次消费者消耗缓冲区中的数据的时候,生产者才能被唤醒,开始往缓冲区添加数据。同样,也可以让消费者在缓冲区空时进入休眠,等到生产者往缓冲区添加数据之后,再唤醒消费者。通常采用进程间通信的方法解决该问题。如果解决方法不够完善,则容易出现死锁的情况。出现死锁时,两个线程都会陷入休眠,等待对方唤醒自己。该问题也能被推广到多个生产者和消费者的情形。
如何解决该问题?
首先来分析下:缓冲区为空则禁止消费者消费,反之禁止生产者生产,同种行为之间互斥。
解决办法:使用两种锁,相应角色的线程需要获取到对应的锁才可以执行,每次执行完后都会唤醒对方阻塞的线程,执行前都需要对缓冲区状态进行判断,不满足则阻塞等待直到被唤醒重新获取锁执行。(当然简单点的话我们可以使用一个锁,锁住整个缓冲区,任何线程彼此互斥)
关于Java实现:1,利用synchronized 和 wait ,notify实现上述所说的一个锁的解决方法。2,利用ReentrentLock 和 Condition 来实现两个锁的解决方法。3,利用BlockingQueue,这种方式最简单,双方持有同一个BlockingQueue对象,不需要任何其它同步操作。关于JUC下不同的BlockingQueue实现原理,请看我关于它们的解析文章
一个例子:蛋糕师做蛋糕,在这个例子里我们使用一个锁,锁住整个缓冲区,任何线程彼此之间互斥,
名字 | 说明 |
---|---|
Main | 测试类 |
MakerThread | 糕点师 |
EaterThread | 客人 |
Table | 桌子 |
public class Main {
public static void main(String[] args) {
Table table = new Table(3);
Thread[] threads = {
new MakerThread("MakerThread-1", table, 31415),
new MakerThread("MakerThread-2", table, 92653),
new MakerThread("MakerThread-3", table, 45862),
new EaterThread("EaterThread-1", table, 75128),
new EaterThread("EaterThread-2", table, 56928),
new EaterThread("EaterThread-3", table, 15648),
};
for (Thread thread : threads){
thread.start();
}
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
}
System.out.println("*******interrupt*******");
// 中断所有线程
for (Thread thread : threads){
thread.interrupt();
}
}
}
蛋糕是这样的:[ Cake No:xxx by MakerThread-x ]
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() {
try {
while (true){
// 随机停顿0-1000毫秒,代表之作蛋糕的耗时
Thread.sleep(random.nextInt(1000));
// 这就是蛋糕 [ Cake No:xxx by MakerThread-x ]
String cake = "[ Cake No:" + nextId() + " by " + getName() + " ]";
table.put(cake);
}
} catch (InterruptedException e) {
}
}
// 由于id是共用的所以用static synchronized,锁为类对象
private static synchronized int nextId(){
return id++;
}
}
EaterThreader
public class EaterThread extends Thread {
private final Random random;
private final Table table;
public EaterThread(String name, Table table, long seed) {
super(name);
this.random = new Random(seed);
this.table = table;
}
@Override
public void run() {
try {
while (true){
// 取蛋糕
table.take();
// 吃蛋糕
Thread.sleep(random.nextInt(1000));
}
} catch (InterruptedException e) {
}
}
}
Table
public class Table {
private final String[] buffer; //一个存放蛋糕的数组
private int head; // 下次put的位置
private int tail; //下次take的位置
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);
while (count >= buffer.length){
System.out.println(Thread.currentThread().getName() + " wait BEGIN");
wait();
System.out.println(Thread.currentThread().getName() + " wait END");
}
buffer[tail] = cake;
tail = (tail + 1) % buffer.length;
count++;
notifyAll();
}
// 拿走蛋糕
public synchronized String take() throws InterruptedException {
while (count <= 0) {
System.out.println(Thread.currentThread().getName() + " wait BEGIN");
wait();
System.out.println(Thread.currentThread().getName() + " wait END");
}
String cake = buffer[head];
head = (head + 1) % buffer.length;
count--;
notifyAll();
System.out.println(Thread.currentThread().getName() + " take " + cake);
return cake;
}
}
来看看执行结果
EaterThread-1 wait BEGIN
EaterThread-3 wait BEGIN
EaterThread-2 wait BEGIN
MakerThread-2 puts [ Cake No:0 by MakerThread-2 ]
EaterThread-2 wait END
EaterThread-2 take [ Cake No:0 by MakerThread-2 ]
EaterThread-3 wait END
EaterThread-3 wait BEGIN
EaterThread-1 wait END
EaterThread-1 wait BEGIN
MakerThread-1 puts [ Cake No:1 by MakerThread-1 ]
EaterThread-1 wait END
EaterThread-1 take [ Cake No:1 by MakerThread-1 ]
EaterThread-3 wait END
EaterThread-3 wait BEGIN
EaterThread-1 wait BEGIN
EaterThread-2 wait BEGIN
MakerThread-2 puts [ Cake No:2 by MakerThread-2 ]
EaterThread-2 wait END
EaterThread-2 take [ Cake No:2 by MakerThread-2 ]
EaterThread-1 wait END
EaterThread-1 wait BEGIN
EaterThread-3 wait END
EaterThread-3 wait BEGIN
MakerThread-3 puts [ Cake No:3 by MakerThread-3 ]
EaterThread-3 wait END
EaterThread-3 take [ Cake No:3 by MakerThread-3 ]
EaterThread-1 wait END
EaterThread-1 wait BEGIN
MakerThread-3 puts [ Cake No:4 by MakerThread-3 ]
EaterThread-1 wait END
EaterThread-1 take [ Cake No:4 by MakerThread-3 ]
MakerThread-1 puts [ Cake No:5 by MakerThread-1 ]
MakerThread-3 puts [ Cake No:6 by MakerThread-3 ]
MakerThread-2 puts [ Cake No:7 by MakerThread-2 ]
EaterThread-1 take [ Cake No:5 by MakerThread-1 ]
EaterThread-2 take [ Cake No:6 by MakerThread-3 ]
MakerThread-1 puts [ Cake No:8 by MakerThread-1 ]
MakerThread-3 puts [ Cake No:9 by MakerThread-3 ]
EaterThread-3 take [ Cake No:7 by MakerThread-2 ]
EaterThread-3 take [ Cake No:8 by MakerThread-1 ]
MakerThread-2 puts [ Cake No:10 by MakerThread-2 ]
MakerThread-3 puts [ Cake No:11 by MakerThread-3 ]
MakerThread-1 puts [ Cake No:12 by MakerThread-1 ]
MakerThread-1 wait BEGIN
EaterThread-1 take [ Cake No:9 by MakerThread-3 ]
MakerThread-1 wait END
EaterThread-3 take [ Cake No:10 by MakerThread-2 ]
MakerThread-1 puts [ Cake No:13 by MakerThread-1 ]
EaterThread-3 take [ Cake No:11 by MakerThread-3 ]
EaterThread-2 take [ Cake No:12 by MakerThread-1 ]
EaterThread-1 take [ Cake No:13 by MakerThread-1 ]
MakerThread-3 puts [ Cake No:14 by MakerThread-3 ]
MakerThread-3 puts [ Cake No:15 by MakerThread-3 ]
MakerThread-2 puts [ Cake No:16 by MakerThread-2 ]
MakerThread-2 puts [ Cake No:17 by MakerThread-2 ]
MakerThread-2 wait BEGIN
EaterThread-3 take [ Cake No:14 by MakerThread-3 ]
MakerThread-2 wait END
MakerThread-3 puts [ Cake No:18 by MakerThread-3 ]
MakerThread-3 wait BEGIN
MakerThread-1 puts [ Cake No:19 by MakerThread-1 ]
MakerThread-1 wait BEGIN
EaterThread-2 take [ Cake No:15 by MakerThread-3 ]
MakerThread-1 wait END
MakerThread-3 wait END
MakerThread-3 wait BEGIN
EaterThread-1 take [ Cake No:16 by MakerThread-2 ]
MakerThread-3 wait END
MakerThread-3 puts [ Cake No:20 by MakerThread-3 ]
MakerThread-3 wait BEGIN
MakerThread-1 puts [ Cake No:21 by MakerThread-1 ]
MakerThread-1 wait BEGIN
EaterThread-3 take [ Cake No:17 by MakerThread-2 ]
MakerThread-1 wait END
MakerThread-3 wait END
MakerThread-3 wait BEGIN
*******interrupt*******
Data
在生产者消费者中,Data角色由Producer角色生成,供Consumer角色使用。在上面例子中就是蛋糕(String)。
Producer生产者
Producer角色生成Data角色,并将其传递给Channel角色。上面例子的MakerThread就是Producer。
Consumer消费者
Consumer角色从Channel中获取Data并使用。上面例子里就是EaterThread。
Channel通道
Channel保管从Producer获取的Data,响应Consumer的请求传递Data。为了为了确保安全,Channel会对Producer和Consumer的访问执行互斥处理。
当Producer将Data传递给Channel,如果Channel状态不适合接受Data,那么Producer将一直等待,直到Channel可以接受为止。
当Consumer从Channel获取Data时,若没有则需一直等待,直到有可获取的Data为止。
存在多个Producer与Consumer时,执行互斥以避免互相之间的影响,它的存在实现了线程的线条运行。
由此可看出Channel承担着传递Data的任务。
对应上面的例子中的Table。
如果不通过Channel直接传递呢?Channel起到通道,中转站的作用
,如果Producer直接调用Consumer的方法,不通过Channel,这样就相当于Producer线程承担了处理所花费的时间,用上面的例子来说就相当于蛋糕师做出一个蛋糕亲手交给客人,然后必须等待客人吃完才能继续做下一个蛋糕,这样程序的相应性变得很差。
而Channel这种方式,Producer便可以不用考虑Consumer,可以持续不断创建Data,当然有存储空间上的限制。
关于wait,需要注意锁的问题,线程进入等待队列时已经释放了锁,当正在wait的线程被调用interrupt方法时,该线程会在重新获取锁后抛出InterruptedEcxception异常,在获取锁之前是不会抛异常的。
关于Thread.interrupted()方法,他是个静态方法,检查并清除中断状态,它的操作对象是当前线程,不能用于清除其它线程的中断状态。
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
该系列文章是《图解Java多线程设计模式》的读书笔记