实验要求
- 选择一个经典的同步问题(生产者-消费者问题、读者-写者问题、哲学家就餐问题等),并模拟实现该同步问题的进程管理;
- 采用信号量机制及相应的P操作、V操作;
- 应避免出现死锁;
- 能够显示相关的状态。
我这里选择的是生产者消费者问题,使用java实现
源码上传到了本人github上
实验原理
代码仿照某个博主的思想重写的,本来想贴出来博主地址,但是忘了是哪位博主,如果日后找到了地址会再贴出来,实在抱歉。
代码结构
- 消费者Consumer作为消费者类,私有属性有id、消费数、仓库,方法有消费,消费实际是调用构造时传入的仓库中的消费方法。
- 生产者Producer作为生产者类,私有属性有id、生产数、仓库,方法有生产,生产实际是调用构造时传入的仓库中的生产方法。
- 仓库Storage作为仓库类,私有属性有一个LinkedList作为商品存放的格子,Max_size是格子数。Storage封装了消费者/生产者对仓库取产品/存产品的方法。在这里,LinkedList是本例中的公共区。
- 主类中实例化了仓库,并用在实例化的时候将该仓库传给了消费者生产者,分别给消费者生产者设置了消费数量
###核心代码 仓库中消费方法
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37public void consume(int num, int id) {
synchronized (list) {
while (list.size() < num) {
System.out.println("【消费者" + id + "】预计取出产品数量:" + num + "\t当前库存:"
+ list.size() + "\t产品太少,不够消费。等待中……");
try {
list.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.print("【消费者" + id + "】预计取出产品数量:" + num + "\n"
+ "【消费者" + id + "】正在取出中……");
//锁住print输出,防止被其他线程的输出打断
synchronized (System.out) {
for (int i = 1; i <= num; ++i) {
try {
sleep(200);
} catch (InterruptedException e) {
e.printStackTrace();
}
list.remove();
System.out.print(i + "...");
}
System.out.println();
}
list.notifyAll();
}
System.out.println("【消费者" + id + "】已取出:" + num + "\t当前库存:" + list.size());
System.out.println("【消费者" + id + "】正在消费产品……");
try {
sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("【消费者" + id + "】消费结束");
}
仓库中生产方法
1 | public void produce(int num, int id) { |
代码思路
其实思路并不复杂,仔细看同步区的代码就会发现,它们基本满足下面代码的格式。
1 | synchronized(){ |
synchronized(list)包裹起来的代码块是同步代码段,含义简单来说即当有线程访问这一部分代码时(实际测试是访问list对象时),会给其加锁,那么其他线程访问list时会被阻塞,等待该线程结束list对象的访问后才能继续访问。
而这里while(条件不满足)则是具体问题的条件,比如消费者想消费30个产品,首先仓库里得有30个产品才行吧?所以需要这里有代码让不满足条件的线程挂起。
对应到操作系统里讲的pv原语,其实java关于pv原语的操作就是synchronized(list)和while字段,我们对应着来看:首先,在操作系统里关于list有一个信号量是是否有人在操作仓库,这个信号量初始值是1,那么synchronized(list)的左大括号和右大括号分别对应这个信号量的p操作和v操作。其次,操作系统中关于仓库的空间大小有限度,那么仓库当前大小这个信号量是0,仓库余空间是100,这两个信号量的pv操作就对应了while部分。在操作系统中,作为生产者,开始拿出产品时需要首先对当前仓库剩余空间这个信号量进行p操作,放一个商品就p操作一次,放完了一个商品就要对仓库当前大小这个信号量进行v操作,100就逐渐减小,0逐渐增大。这里的p和v分别对应while部分,当100减到0的时候,没有空间放商品了,while条件不再满足,那么该线程需要被挂起,生产者就得等待消费者先来消费。消费者是同理,这里不予赘述。
另外我代码里有些输出也被synchronized包裹起来了,这里解释一下是因为print()方法是会被异步执行的,也就是说我本来预定某个生产者/消费者隔0.3s输出1...2...3...这样模拟存入/取出的过程,如果在输出的时候正好有某个消费者/生产者它生产/消费结束了,那么它也要输出一行话叫“生产结束”/“消费结束”,如果不包裹起来,输出会被打断,出现这样尴尬的情况 >【生产者2】正在存入中……1…2…3…4…5…6…7…8…【生产者1】生产结束 >9...10...11…12…13…14…15…16…17…18…19…20…21…22…23…24…25…26…27…28…29…30… >【生产者2】已存入:30 当前库存:30
这里就是生产者1发出其生产结束准备存入产品的消息打断了正在存入产品的生产者2的输出,将这个输出过程加上synchronized(System.out)并包裹起来即可解决。(但是这样产生的问题是生产者1生产完成后不能能及时的打印出自己生产完成了的讯息。嘛毕竟打印机只有一台,只能这么解决了)
代码结果
【生产者1】正在生产产品…… 【生产者2】正在生产产品…… 【生产者3】正在生产产品…… 【生产者4】正在生产产品…… 【消费者1】预计取出产品数量:50 当前库存:0 产品太少,不够消费。等待中…… 【消费者2】预计取出产品数量:20 当前库存:0 产品太少,不够消费。等待中…… 【消费者3】预计取出产品数量:30 当前库存:0 产品太少,不够消费。等待中…… 【生产者2】生产结束 【生产者2】预计存入仓库数量:30 【生产者2】正在存入中……1...2...3...4...5...6...7...8...9...10...11...12...13...14...15...16...17...18...19...20...21...22...23...24...25...26...27...28...29...30... 【生产者2】已存入:30 当前库存:30 【生产者1】生产结束 【消费者3】预计取出产品数量:30 【消费者3】正在取出中……1...2...3...4...5...6...7...8...9...10...11...12...13...14...15...16...17...18...19...20...21...22...23...24...25...26...27...28...29...30... 【消费者3】已取出:30 当前库存:0 【生产者3】生产结束 【生产者4】生产结束 【消费者2】预计取出产品数量:20 当前库存:0 产品太少,不够消费。等待中…… 【消费者3】正在消费产品…… 【消费者1】预计取出产品数量:50 当前库存:0 产品太少,不够消费。等待中…… 【生产者4】预计存入仓库数量:30 【生产者4】正在存入中……1...2...3...4...5...6...7...8...9...10...11...12...13...14...15...16...17...18...19...20...21...22...23...24...25...26...27...28...29...30... 【消费者3】消费结束 【生产者4】已存入:30 当前库存:30 【生产者3】预计存入仓库数量:30 【生产者3】正在存入中……1...2...3...4...5...6...7...8...9...10...11...12...13...14...15...16...17...18...19...20...21...22...23...24...25...26...27...28...29...30... 【生产者3】已存入:30 当前库存:60 【生产者1】预计存入仓库数量:30 【生产者1】正在存入中……1...2...3...4...5...6...7...8...9...10...11...12...13...14...15...16...17...18...19...20...21...22...23...24...25...26...27...28...29...30... 【生产者1】已存入:30 当前库存:90 【消费者1】预计取出产品数量:50 【消费者1】正在取出中……1...2...3...4...5...6...7...8...9...10...11...12...13...14...15...16...17...18...19...20...21...22...23...24...25...26...27...28...29...30...31...32...33...34...35...36...37...38...39...40...41...42...43...44...45...46...47...48...49...50... 【消费者1】已取出:50 当前库存:40 【消费者1】正在消费产品…… 【消费者2】预计取出产品数量:20 【消费者2】正在取出中……1...2...3...4...5...6...7...8...9...10...11...12...13...14...15...16...17...18...19...20... 【消费者2】已取出:20 当前库存:20 【消费者2】正在消费产品…… 【消费者1】消费结束 【消费者2】消费结束
Process finished with exit code 0
马后炮
调试过程
这一部分记录我自己在写代码时踩到的坑。
关于synchronized到底同步的是啥,经过多方询问以及自己测试,最终发现确实有些是synchronized包裹的代码段也会被异步执行,所以synchronized同步的是括号中的对象,对于不是这个对象的操作是会被异步执行的。假设我有句输出在这个代码段里,那么它是可以被直接访问到的,而不会被加锁。也就是synchronized锁是锁在了括号里的对象上。其实这一点也很好理解,一个方面来说这正好是pv原语的体现,pv原语只对信号量操作,而不关注代码本身,而synchronized本身锁住的是某个对象,而不是代码也正好能体现pv操作。另一个方面,在多线程处理的时候,本身就应该尽可能的减小锁的粒度,不是同步所需要的万不得已之时,尽可能的少去使用锁,这样才能加大效率。
存在的问题
其实这个代码的封装程度不算高,我有意将对仓库的存入和拿出操作写在里仓库里,不过问题在于没有将存入和取出这两个对仓库的动作以及生产和消费这两个消费者生产者本身的操作分开。而是通通放在了仓库的消费、生产操作里。其实我写到了后面才发现,生产商品和把生产的东西放进仓库应该要分开来写,在代码结果里可以看到我已经做了划分,然而更好的解决方案应该是把生产的代码整个封装到生产者类里,把消费的代码整个封装到消费者类里。这样仓库类就是单纯的做存入和取出操作,降低代码耦合度。
小结
在进程间通讯、synchronized方面查阅了许多资料才最终理解了java多线程的具体实现原理,最后对整个概念都有了进一步的认识。最有意思的是自己试着将操作系统的pv操作和java多线程的同步进行了比较和对应,其实可以认为java中synchronized等对pv原语的封装实际也是为了简化pv操作。