【大学】并发死锁与进程

合集下载
  1. 1、下载文档前请自行甄别文档内容的完整性,平台不提供额外的编辑、内容补充、找答案等附加服务。
  2. 2、"仅部分预览"的文档,不可在线预览部分如存在完整性等问题,可反馈申请退款(可完整预览的文档不适用该条件!)。
  3. 3、如文档侵犯您的权益,请联系客服反馈,我们会尽快为您处理(人工客服工作时间:9:00-18:30)。

【关键字】大学
黑龙江大学
实验报告
黑龙江大学教务处
一、实验目的
1、加深对进程感念的理解,明确进程和程序的区别。

2、进一步认识并发执行的实质。

3、分析进程争用资源的现象,学习解决进程互斥的方法。

二、实验环境
1、编程语言:Java语言
2、开发环境:JDK IDEA 2016.2、macOS
三、实验内容
1、问题描述
哲学家就餐问题可以这样表述,假设有五位哲学家围坐在一张圆形餐桌旁,做以下两件事情之一:吃饭,或者思考。

吃东西的时候,他们就停止思考,思考的时候也停止吃东西。

餐桌中间有一大碗意大利面,每两个哲学家之间有一只餐叉。

2、问题分析
哲学家从来不交谈,这就很危险,可能产生,每个哲学家都拿着左手的餐叉,永远都在等右边的餐叉(或者相反)。

即使没有死锁,也有可能发生。

例如,假设规定当哲学家等待另一只餐叉超过五分钟后就放下自己手里的那一只餐叉,并且再等五分钟后进行下一次尝试。

这个策略消除了死锁(系统总会进入到下一个状态),但仍然有可能发生“活锁”。

3、死锁现象程序举例:
(1)死锁程序清单:
程序清单1-1 Chopstick.java
package ;
public class Chopstick {
private boolean taken = false;//判断是此筷子是否被拿起
public boolean isTaken() {
return taken;
}
public synchronized void take() throws InterruptedException {
while (taken) {
//如果已被拿起,则等待
wait();
}
//如果没有被拿起,则可以被拿起,并设置taken为true
taken = true;
}
public synchronized void drop() {
//放下筷子之后设置taken为false,并通知其他筷子
notifyAll();
}
}
程序清单1-2 Philosopher.java
package ;
import ;
import ;
public class Philosopher implements Runnable {
private Chopstick left;//左筷子
private Chopstick right;//右筷子
private final int id;//哲学家编号
private final int ponderFactor;//根据这个属性设置思考时间
private Random rand = new Random(250);
public Philosopher(Chopstick left, Chopstick right, int ident, int ponder) {
this.left = left;
this.right = right;
this.id = ident;
this.ponderFactor = ponder;
}
public void run() {
try {
while (true) {
+ " " + "thinking");
Thread.sleep(ponderFactor * rand.nextInt(250)); right.take();
+ " " + "拿右筷子");
left.take();
+ " " + "拿左筷子");
Thread.sleep(ponderFactor * rand.nextInt(250)); + " " + "吃");
right.drop();
+ " " + "放下右筷子");
left.drop();
+ " " + "放下左筷子");
}
} catch (InterruptedException e) {
+ " 退出 ");
}
}
public String toString() {
return "Phiosopher : " + id;
}
}
程序清单1-3 DeadlockingDiningPhilosophers.java package ;
import ;
import ;
import ;
public class DeadlockingDiningPhilosophers {
public static void main(String[] args) throws InterruptedException {
int ponder = 5;
int size = 5;
ExecutorService exec = Executors.newCachedThreadPool(); Chopstick[] stick = new Chopstick[size];
for (int i = 0; i < size; i++) {
stick[i] = new Chopstick();
}
for (int i = 0; i < size; i++) {
Philosopher p =
new Philosopher(stick[i], stick[(i + 1) % size], i, ponder);
exec.execute(p);
}
}
}
(2)死锁现象分析
上面的程序清单中所示程序极有可能发生死锁现象,例如当所有的哲学家同时拿起左手的筷子(或同时拿起右手的筷子),那么任何人都不可能再拿起另一只手的筷子,所有哲学家都进入了等待状态(阻塞队列),那么死锁就发生了。

4、资源分级解法
(1)解法概述:
一个简单的解法是为资源(这里是餐叉)分配一个偏序或者分级的关系,并约定所有资源都按照这种顺序获取,按相反顺序释放,而且保证不会有两个无关资源同时被同一项工作所需要。

在哲学家就餐问题中,资源(餐叉)按照某种规则编号为1至5,每一个工作单元(哲学家)总是先拿起左右两边编号较低的餐叉,再拿编号较高的。

用完餐叉后,他总是先放下编号较高的餐叉,再放下编号较低的。

在这种情况下,当四位哲学家同时拿起他们手边编号较低的餐叉时,只有编号最高的餐叉留在桌上,从而第五位哲学家就不能使用任何一只餐叉了。

而且,只有一位哲学家能使用最高编号的餐叉,所以他能使用两只餐叉用餐。

当他吃完后,他会先放下编号最高的餐叉,再放下编号较低的餐叉,从而让另一位哲学家拿起后边的这只开始吃东西。

(2)程序分析:
例如上述程序中,我们不妨规定前四位哲学家先拿左手边的筷子,再拿右手边的筷子,而第五位(或最后一位)哲学家先拿右手边的筷子,再拿左手边的筷子,这样就可以轻松的解决死锁的问题,我们只需要对上述程序清单1-3稍作修改,修改后的内容如下。

程序清单2-1 DeadlockingDiningPhilosophers.java
package ;
import ;
import ;
import ;
public class DeadlockingDiningPhilosophers {
public static void main(String[] args) throws InterruptedException {
int ponder = 5;
int size = 5;
ExecutorService exec = Executors.newCachedThreadPool(); Chopstick[] stick = new Chopstick[size];
for (int i = 0; i < size; i++) {
stick[i] = new Chopstick();
}
for (int i = 0; i < size; i++) {
if(i < size - 1) {
Philosopher p =
new Philosopher(stick[i], stick[(i+1)%size], i, ponder);
exec.execute(p);
}else {
Philosopher p = new Philosopher(stick[0], stick[i], i, ponder);
exec.execute(p);
}
}
}
}
(3)劣势分析:
尽管资源分级能避免死锁,但这种策略并不总是实用的,特别是当所需资源的列表并不是事先知道的时候。

例如,假设一个工作单元拿着资源3和5,并决定需要资源2,则必须先要释放5,之后释放3,才能得到2,之后必须重新按顺序获取3和5。

对需要访问大量数据库记录的计算机程序来说,如果需要先释放高编号的记录才能访问新的记录,那么运行效率就不会高,因此这种方法在这里并不实用。

5、锁变量解法(服务员解法)
(1)解法概述:
一个简单的解法是引入一个餐厅服务生,哲学家必须经过他的允许才能拿起餐叉。

因为服务生知道哪只餐叉正在使用,所以他能够作出判断避免死锁。

为了演示这种解法,假设哲学家依次标号为A至E。

如果A和C在吃东西,则有四只餐叉在使用中。

B坐在A和C之间,所以两只餐叉都无法使用,而D和E 之间有一只空余的餐叉。

假设这时D想要吃东西。

如果他拿起了第五只餐叉,就有可能发生死锁。

相反,如果他征求服务生同意,服务生会让他等待。

这样,我们就能保证下次当两把餐叉空余出来时,一定有一位哲学家可以成功的得到一对餐叉,从而避免了死锁。

(2)程序分析:详细解法程序代码如下所示。

程序清单3-1 Folk.java
package ;
public class Folk {
// 叉子编号
private int id;
// 叉子注册状态(是否被拿起)
private boolean reg;
public boolean isReg() {
return reg;
}
public void setReg(boolean reg) {
this.reg = reg;
}
public int getId(){
return id;
}
public Folk(int id){
super();
this.id = id;
}
@Override
public String toString() {
return id + "号叉子";
}
}
程序清单3-2 Philosopher.java
package ;
import ;
/**
* 哲学家类
*/
public class Philosopher {
// 哲学家编号
private int id;
// 哲学家左右两边的叉子
private Folk[] folks;
// 用于产生随机数的random实例
private Random random;
public Philosopher(int id, Folk leftFolk, Folk rightFolk) { super();
this.id = id;
this.folks = new Folk[2];
this.folks[0] = leftFolk;
this.folks[1] = rightFolk;
this.random = new Random();
}
public int getId() {
return id;
}
public Folk[] getFolks() {
return folks;
}
// 模拟思考
public void thinking() {
try {
"哲学家 " + id + " 正在思考");
Thread.sleep(random.nextInt(2) * 1000);
"哲学家 " + id + " 思考结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// 模拟就餐
public void eat(){
try {
"哲学家 " + id + " 开始就餐");
Thread.sleep(random.nextInt(5) * 1000);
"哲学家 " + id + " 就餐结束");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
程序清单3-3 Waiter.java
package ;
/**
* 服务员类
*/
public class Waiter {
// 向服务员申请叉子
public synchronized void takeFolks(Philosopher philosopher) { "哲学家 " + philosopher.getId()
+ " 向服务员申请叉子");
try {
// 判断两个叉子是否均可以拿起
while (philosopher.getFolks()[0].isReg()
|| philosopher.getFolks()[1].isReg()) {
// 只要叉子有其中一只已经被注册(使用中),则申请失败
"哲学家 " + philosopher.getId()
+ " 未能申请到叉子,等待中....");
// 如果未申请到则等待
wait();
}
} catch (InterruptedException e) {
e.printStackTrace();
}
// 拿起两只叉子(注册)
philosopher.getFolks()[0].setReg(true);
philosopher.getFolks()[1].setReg(true);
"哲学家 " + philosopher.getId()
+ " 成功申请到叉子-------√");
}
public synchronized void putFolks(Philosopher philosopher) { // 放下两只叉子(取消注册)
philosopher.getFolks()[0].setReg(false);
philosopher.getFolks()[1].setReg(false);
"哲学家 " + philosopher.getId()
+ " 已放下叉子");
// 唤醒等待中的哲学家
notifyAll();
}
}
程序清单3-4 Repast.java
package ;
/**
* 就餐线程
*/
public class Repast implements Runnable {
// 当前就餐情景中的服务员
private Waiter waiter;
// 就餐的哲学家
private Philosopher philosopher;
public Repast(Waiter waiter, Philosopher philosopher){ super();
this.waiter = waiter;
this.philosopher = philosopher;
}
/**
* 哲学家就餐线程体
*/
@Override
public void run() {
try {
while(!Thread.interrupted()){
// 模拟哲学家思考
philosopher.thinking();
// 向服务员申请叉子
waiter.takeFolks(philosopher);
// 模拟哲学家就餐
philosopher.eat();
// 哲学家申请放下叉子
waiter.putFolks(philosopher);
}
} catch (Exception e) {
"线程中断,退出");
}
}
}
程序清单3-5 Main.java package ;
import ;
import ;
/**
*/
public class Main {
public static void main(String[] args) { // 就餐人数
int size = 5;
// 构造服务员实例
Waiter waiter = new Waiter();
// 构造叉子
Folk[] folks = new Folk[size];
for (int i = 0; i < size; i++) {
folks[i] = new Folk(i + 1);
}
// 带缓冲的线程池
ExecutorService exec = Executors.newCachedThreadPool();
// 循环创建就餐线程并执行
for (int i = 0; i < size; i++) {
Philosopher philosopher =
new Philosopher(i + 1, folks[i], folks[(i + 1) % size]);
Repast r = new Repast(waiter, philosopher);
exec.execute(r);
}
}
}
(3)劣势分析
本算法同样可以解决死锁问题,不过它和资源分级解法有一个共同的特点,并发性能不强,对于较大数据来说,单一锁变量的执行效率不高。

6、Chandy/Misra解法
1984年,K. Mani Chandy和J. Misra提出了哲学家就餐问题的另一个解法,允许任意的用户(编号P1, ..., Pn)争用任意数量的资源。

与迪科斯彻的解法不同的是,这里编号可以是任意的。

(1)对每一对竞争一个资源的哲学家,新拿一个餐叉,给编号较低的哲学家。

每只餐叉都是“干净的”或者“脏的”。

最初,所有的餐叉都是脏的。

(2)当一位哲学家要使用资源(也就是要吃东西)时,他必须从与他竞争的
邻居那里得到。

对每只他当前没有的餐叉,他都发送一个请求。

(3)当拥有餐叉的哲学家收到请求时,如果餐叉是干净的,那么他继续留着,否则就擦干净并交出餐叉。

(4)当某个哲学家吃东西后,他的餐叉就变脏了。

如果另一个哲学家之前请求过其中的餐叉,那他就擦干净并交出餐叉。

这个解法允许很大的并行性,适用于任意大的问题。

(二)生产者-消费者问题
1、问题描述
生产者消费者问题(英语:Producer-consumer problem),也称有限缓冲问题(英语:Bounded-buffer problem),是一个多线程同步问题的经典案例。

该问题描述了两个共享固定大小缓冲区的线程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。

生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。

与此同时,消费者也在缓冲区消耗这些数据。

该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。

2、常见算法
要解决该问题,就必须让生产者在缓冲区满时休眠(要么干脆就放弃数据),等到下次消费者消耗缓冲区中的数据的时候,生产者才能被唤醒,开始往缓冲区添加数据。

同样,也可以让消费者在缓冲区空时进入休眠,等到生产者往缓冲区添加数据之后,再唤醒消费者。

通常采用进程间通信的方法解决该问题,常用的方法有信号灯法、管程法等。

如果解决方法不够完善,则容易出现死锁的情况。

出现死锁时,两个线程都会陷入休眠,等待对方唤醒自己。

该问题也能被推广到多个生产者和消费者的情形。

3、代码实现(管程法):
程序清单4-1 Product.java
package ;
/**产品类
* Created by huzhanfei on 2016/11/2.
*/
public class Product {
int id;//产品编号
Product(int id) {
this.id = id;
}
public String toString() {
return "编号" + id + "的产品";
}
}
程序清单4-2 SyncStack.java package ;
/**装产品的栈,管程类
* Created by huzhanfei on 2016/11/2.
*/
public class SyncStack{
int index = 0;
Product[] stb = new Product[6];//构造数组,容量是6 //入栈
public synchronized void push(Product sb){
while(index==stb.length){//栈满
try {
"产品生产过量,等待消费者消费!");
this.wait();//让当前线程等待
} catch (InterruptedException e) {
e.printStackTrace();
}
}
//唤醒在此对象监视器上等待的单个线程,即消费者线程 this.notify();
stb[index] = sb;
this.index++;
}
//出栈
public synchronized Product pop(){
while(index==0){//栈空
try {
"产品脱销,消费者等待工厂供货");
this.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
this.notify();
//push第n个之后,this.index++,使栈顶为n+1,故return之前要减一 this.index--;
return stb[index];
}
}
程序清单4-3 Producer.java
package ;
import ;
/**生产者类,实现了Runnable接口,以便于构造生产者线程
* Created by huzhanfei on 2016/11/2.
*/
public class Producer implements Runnable{
SyncStack ss;
String id;
Producer(String id,SyncStack ss){
this.id = id;
this.ss = ss;
}
@Override
public void run() {
// 开始生产产品
int i = 1;
while(true){
Product stb = new Product(i);
ss.push(stb);
"编号为"+id+"的生产者生产了"+stb);
i++;
try {
Thread.sleep(45);//每生产一个产品,等待10毫秒 } catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
程序清单4-4 Consume.java
package ;
import ;
/**
* 消费者类,实现了Runnable接口,以便于构造消费者线程
*/
public class Consume implements Runnable{
String id; //消费者编号
SyncStack ss;
public Consume(String id, SyncStack ss) {
this.id = id;
this.ss = ss;
}
@Override
public void run() {
while(true){//开始消费
Product stb = ss.pop();
"编号为"+id+"的消费者消费了"+stb);
try {
//每消费一个产品,随机休眠30-60毫秒。

Thread.sleep(new Random().nextInt(30)+30); } catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
程序清单4-5 ProduceConsume.java package ;
import ;
import ;
/**
* Created by huzhanfei on 2016/11/2.
*/
public class ProduceConsume {
public static void main(String[] args) {
//建造一个栈
SyncStack ss = new SyncStack();
//新建一个生产者,使之持有栈
Producer p = new Producer("PRODUCER001",ss);
//新建一个消费者1,使之持有同一个栈
Consume c1 = new Consume("CONSUME001",ss);
//创建一个生产者与消费者线程池
ExecutorService exec = Executors.newCachedThreadPool(); exec.execute(c1);
exec.execute(p);
}
}
四、实验总结
本实验通过使用线程模拟操作系统中的进程加深了我对进程感念的理解,明确了进程和程序的区别,使得我进一步认识并发执行的实质,通过分析进程争用资源的现象,总结了解决进程互斥的方法。

此文档是由网络收集并进行重新排版整理.word可编辑版本!。

相关文档
最新文档