假设一个系统有 三个抽烟者进程和一个供应者进程 \color{red}三个抽烟者进程和一个供应者进程 三个抽烟者进程和一个供应者进程。
每个抽烟者不停地卷烟并抽掉它,但是要卷起并抽掉一支烟,抽烟者需要有三种材料:烟草、纸和胶水。三个抽烟者中,第一个拥有烟草、第二个拥有纸、第三个拥有胶水。供应者进程无限地提供三种材料,供应者每次将两种材料放桌子上,拥有剩下那种材料的抽烟者卷一根烟并抽掉它,并给供应者进程一个信号告诉完成了,供应者就会放另外两种材料再桌上,这个过程一直重复(让三个抽烟者轮流地抽烟)
1、问题分析
本质上这题也属于“生产者-消费者”问题,更详细的说应该是“可生产多种产品的单生产者-多消费者”。
① 关系分析。找出题目中描述的各个进程,分析它们之间的同步、互斥关系。
桌子可以抽象为容量为 1 的缓冲区,要互斥访问
- 组合一:纸+胶水
- 组合二:烟草+胶水
- 组合三:烟草+纸
同步关系(从事件的角度来分析):
-
桌上有组合一:第一个抽烟者取走东西
-
桌上有组合二:第二个抽烟者取走东西
-
桌上有组合三:第三个抽烟者取走东西
-
发出完成信号:供应者将下一个组合放到桌上
② 整理思路。根据各进程的操作流程确定 P
、V
操作的大致顺序
- 同步关系:前
V
后P
③ 设置信号量。设置需要的信号量,并根据题目条件确定信号量初值。
- (互斥信号量初值一般为 1,同步信号量的初始值要看对应资源的初始值是多少)
2、具体实现
由于互斥资源只有一个,所以上述案例可以不需要互斥量
上述所示,若 P(finish)
放在 if
前面,则需要设置 finish
初值为
1
1
1,否则会产生死锁
- 放在
if
前面就跟之前案例中的盘子互斥资源一样了
缓冲区大小为 1,同一时刻,四个同步信号量中至多有一个的值为1
3、Java 案例
import java.util.Random;
import java.util.concurrent.*;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
class ProducerConsumerByLock {
//互斥资源, 由于直接在修改 i 的时候加锁了, 所以不需要设置为原子类
private static int i = 1;
//最大容量
private static int capacity = 1;
//产品个数
private static int count = 0;
//flag
private static Lock lock = new ReentrantLock(false);
private static Condition offer1 = lock.newCondition();
private static Condition offer2 = lock.newCondition();
private static Condition offer3 = lock.newCondition();
private static Condition finish = lock.newCondition();
public static void main(String[] args) throws ExecutionException, InterruptedException {
//三个吸烟者
for (int i = 1; i <= 3; i++) {
CompletableFuture.runAsync(new Smokers(i));
}
Thread.sleep(1);
//一个生产者
CompletableFuture.runAsync(new Producer());
//CompletableFuture.runAsync(new Consumer()); 产生的进程默认是守护进程
try {
TimeUnit.SECONDS.sleep(10);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//生产者
public static class Producer implements Runnable {
public void produce() {
while (true) {
lock.lock();
try {
while (capacity == count) {
System.out.println("产品 " + i + " 还没有被消费,阻塞");
finish.await();
}
count++;
//根据 i 的值不同唤醒不同的进程
switch (i) {
case 1:
offer1.signalAll();
break;
case 2:
offer2.signalAll();
break;
case 3:
offer3.signalAll();
break;
default:
}
i = (i + 1) % 3 == 0 ? 3 : (i + 1) % 3;
System.out.println("生产当前一个产品: " + i);
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
try {
Thread.sleep(new Random().nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Override
public void run() {
produce();
}
}
//吸烟者
@AllArgsConstructor
public static class Smokers implements Runnable {
private int number;
public void smoke() {
while (true) {
lock.lock();
try {
//如果当前吸烟者对应的材料不同, 阻塞
while (count == 0 || i != number) {
if (count == 0) {
System.out.println("吸烟者" + number + "阻塞, 材料为空");
} else {
System.out.println("吸烟者" + number + "阻塞, 当前材料: " + i);
}
switch (i) {
case 1:
offer1.await();
break;
case 2:
offer2.await();
break;
case 3:
offer3.await();
break;
default:
//刚开始默认全部阻塞
switch (number) {
case 1:
offer1.await();
break;
case 2:
offer2.await();
break;
case 3:
offer3.await();
break;
default:
}
}
}
count--;
//进行吸烟
System.out.println("吸烟者" + number + "吸烟 " + i + ", 吸烟完成");
//唤醒生产者进行生产下一个吸烟者
finish.signalAll();
} catch (InterruptedException e) {
e.printStackTrace();
} finally {
lock.unlock();
}
//防止一瞬间消费完成
try {
Thread.sleep(new Random().nextInt(1000));
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
@Override
public void run() {
smoke();
}
}
}
stdout:
吸烟者1阻塞, 材料为空
吸烟者2阻塞, 材料为空
吸烟者3阻塞, 材料为空
生产当前一个产品: 2
吸烟者1阻塞, 当前材料: 2
吸烟者2吸烟 2, 吸烟完成
吸烟者3阻塞, 材料为空
生产当前一个产品: 3
吸烟者1阻塞, 当前材料: 3
吸烟者3吸烟 3, 吸烟完成
吸烟者2阻塞, 材料为空
吸烟者3阻塞, 材料为空
生产当前一个产品: 1
吸烟者1吸烟 1, 吸烟完成
吸烟者2阻塞, 材料为空
吸烟者3阻塞, 材料为空
生产当前一个产品: 2
吸烟者2吸烟 2, 吸烟完成
吸烟者3阻塞, 材料为空
吸烟者2阻塞, 材料为空
生产当前一个产品: 3
吸烟者3吸烟 3, 吸烟完成
吸烟者2阻塞, 材料为空
吸烟者1阻塞, 材料为空
生产当前一个产品: 1
吸烟者2阻塞, 当前材料: 1
吸烟者1吸烟 1, 吸烟完成
吸烟者3阻塞, 材料为空
生产当前一个产品: 2
吸烟者2吸烟 2, 吸烟完成
吸烟者3阻塞, 材料为空
生产当前一个产品: 3
吸烟者3吸烟 3, 吸烟完成
生产当前一个产品: 1
吸烟者1吸烟 1, 吸烟完成
吸烟者3阻塞, 材料为空
吸烟者2阻塞, 材料为空
吸烟者1阻塞, 材料为空
生产当前一个产品: 2
吸烟者3阻塞, 当前材料: 2
吸烟者2吸烟 2, 吸烟完成
吸烟者1阻塞, 材料为空
生产当前一个产品: 3
吸烟者3吸烟 3, 吸烟完成
吸烟者1阻塞, 材料为空
吸烟者2阻塞, 材料为空
生产当前一个产品: 1
吸烟者1吸烟 1, 吸烟完成
吸烟者2阻塞, 材料为空
吸烟者3阻塞, 材料为空
生产当前一个产品: 2
吸烟者2吸烟 2, 吸烟完成
吸烟者3阻塞, 材料为空
吸烟者2阻塞, 材料为空
生产当前一个产品: 3
吸烟者3吸烟 3, 吸烟完成
吸烟者2阻塞, 材料为空
吸烟者1阻塞, 材料为空
吸烟者3阻塞, 材料为空
生产当前一个产品: 1
吸烟者2阻塞, 当前材料: 1
吸烟者1吸烟 1, 吸烟完成
吸烟者3阻塞, 材料为空
吸烟者1阻塞, 材料为空
生产当前一个产品: 2
吸烟者2吸烟 2, 吸烟完成
吸烟者3阻塞, 材料为空
吸烟者1阻塞, 材料为空
生产当前一个产品: 3
吸烟者3吸烟 3, 吸烟完成
吸烟者1阻塞, 材料为空
吸烟者2阻塞, 材料为空
生产当前一个产品: 1
吸烟者1吸烟 1, 吸烟完成
吸烟者2阻塞, 材料为空
吸烟者3阻塞, 材料为空
吸烟者1阻塞, 材料为空
生产当前一个产品: 2
吸烟者2吸烟 2, 吸烟完成
吸烟者3阻塞, 材料为空
吸烟者1阻塞, 材料为空
4、总结
吸烟者问题可以为我们解决“可以生产多个产品的单生产者”问题提供一个思路。
值得吸取的精华是:“轮流让各个吸烟者吸烟” 必然需要 “轮流的在桌上放上组合一、二、三”,注意体会我们是如何用一个整型变量i 实现这个“轮流”过程的。
如果题目改为 “每次随机地让一个吸烟者吸烟”,我们有应该如何用代码写出这个逻辑呢?
random
变量即可
若一个生产者要生产多种产品(或者说会引发多种前驱事件),那么各个 V
操作应该放在各自对应的 “前驱事件” 发生之后的位置。