前言
上篇文章介绍了线程的同步,以及如何实现,感兴趣的可以点进主页去查看。为了复习昨天的知识点顺便来学习一下操作系统中经典的生产者消费者问题以及死锁问题。先来说一下死锁,假如有一对双胞胎,现在有一个遥控汽车,哥哥拿到了遥控器,而弟弟拿到小汽车,哥哥想要弟弟的汽车,而弟弟想要哥哥的遥控器,两个人谁也不让谁,结果只能僵持下去,谁也玩不了玩具车。在程序员看来,这个过程不就是一个“死锁”吗。
一、什么是死锁?
1.定义
死锁是指两个或两个以上的进程在执行过程中,由于竞争资源或者彼此通信而造成的一种阻塞的现象,若无外力作用,他们都将无法推进下去。此时称系统处于死锁状态或系统产生了死锁,这些永远在互相等待的进程称为死锁进程。它会导致大量的系统资源浪费,甚至导致系统崩溃。
2.产生的原因
死锁并不是随便就可以产生的,而是要满足一些特定条件的:
- 互斥条件:一个资源每次只能被一个进程使用
- 请求和保持条件:一个进程因请求资源而阻塞时,对已获得的资源保持不放
- 不可剥夺条件:进程已获得的资源,在末使用完之前,不能强行剥夺
- 环路等待条件:若干进程之间形成一种头尾相接的循环等待资源关系
我们再来了解一下可剥夺资源和不可剥夺资源系统中的资源可以分为两类:一类是可剥夺资源,是指某进程在获得这类资源后,该资源可以再被其他进程或系统剥夺。例如,优先权高的进程可以剥夺优先权低的进程的处理机。另一类资源是不可剥夺资源,当系统把这类资源分配给某进程后,再不能强行收回,只能在进程用完后自行释放,如磁带机、打印机等。
在系统中所配置的不可剥夺资源,由于它们的数量不能满足诸进程运行的需要,会使进程在运行过程中,因争夺这些资源而陷于僵局。例如,系统中只有一台打印机R1和一台磁带机R2,可供进程P1和P2共享。假定PI已占用了打印机R1,P2已占用了磁带机R2,若P2继续要求打印机R1,P2将阻塞;P1若又要求磁带机,P1也将阻塞。于是,在P1和P2之间就形成了僵局,两个进程都在等待对方释放自己所需要的资源,但是它们又都因不能继续获得自己所需要的资源而不能继续推进,从而也不能释放自己所占有的资源,以致进入死锁状态。
下面我们用代码来演示一下死锁的形成,代码如下:
/**
* 两个线程A,B,两个不可剥夺资源1,2(用synchronized锁模拟),
* 先让A获取1号资源,再尝试获取2号资源,
* 让B先获取2号资源,再尝试获取1号资源,就会形成死锁
*
*/
public class DeadLock {
public static void main(String[] args) {
deadlock();
}
private static void deadlock() {
// 两个资源
final Object resource1 = "resource1";
final Object resource2 = "resource2";
//为了演示方便,先让第一个线程占有资源1,然后休眠,让另一个线程去占有资源2,方便形成死锁
Thread A = new Thread() {
public void run() {
synchronized (resource1) {
System.out.println("A线程拿到资源1");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 尝试占有资源2,如果不能占有,该线程会一直等待
synchronized (resource2) {
System.out.println("A线程拿到资源2");
}
}
}
};
//为了演示方便,先让第二个线程占有资源2,然后休眠,让第一个线程去占有资源1,方便形成死锁
Thread B = new Thread() {
public void run() {
synchronized (resource2) {
System.out.println("B线程拿到资源2");
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 尝试占有资源1,如果不能占有,该线程会一直等待
synchronized (resource1) {
System.out.println("B线程拿到资源1");
}
}
}
};
// 启动线程
A.start();
B.start();
}
}
两个都拿不到自己需要进行下一步的资源,就会导致两个线程都无法执行下去,形成死锁。运行截图如下:
3.解决办法
既然知道了死锁产生的必要条件,就可以最大可能的预防、避免和解除死锁。死锁预防是一种较简单和直观的事先预防的解决方法。方法是通过设置某些限制条件,去破坏产生死锁的四个必要条件中的一个或者几个,来预防发生死锁。这里介绍一下有序资源分配法。
有序资源分配法
这种算法资源按某种规则系统中的所有资源统一编号(例如打印机为1、磁带机为2、磁盘为3、等等),申请时必须以上升的次序。系统要求申请进程:
①对它所必须使用的而且属于同一类的所有资源,必须一次申请完;
②在申请不同类资源时,必须按各类设备的编号依次申请。例如:进程PA,使用资源的顺序是R1,R2; 进程PB,使用资源的顺序是R2,R1;若采用动态分配有可能形成环路条件,造成死锁。
采用有序资源分配法:R1的编号为1,R2的编号为2;
PA:申请次序应是:R1,R2
PB:申请次序应是:R1,R2
这样就破坏了环路条件,避免了死锁的发生。同时我们可以还可以采用操作系统经典问题银行家算法来避免死锁。
银行家算法
避免死锁算法中最有代表性的算法是Dijkstra E.W 于1968年提出的银行家算法,它以银行借贷系统的分配策略为基础,判断并保证系统的安全运行。在避免死锁方法中允许进程动态地申请资源,系统在每次实施资源分配之前,先计算资源分配的安全性,若此次资源分配安全(即资源分配后,系统能按某种顺序来为每个进程分配其所需的资源,直至最大需求,使每个进程都可以顺利地完成),便将资源分配给进程,否则不分配资源,让进程等待。
还有就是事先不采取任何措施,在死锁发生后,可通过系统所设置的检测机构,及时地检测出死锁的发生,并精确地确定与死锁有关的进程和资源。检测方法包括定时检测、效率低时检测、进程等待时检测等。然后采取适当措施,从系统中将已发生的死锁清除掉。当检测到系统中已发生死锁时,须将进程从死锁状态中解脱出来。常用的实施方法是撤销或挂起一些进程,以便回收一些资源,再将这些资源分配给已处于阻塞状态的进程,使之转为就绪状态,以继续运行。死锁的检测和解除措施,有可能使系统获得较好的资源利用率和吞吐量,但在实现上难度也最大。
二、生产者消费者问题
生产者消费者问题(英语:Producer-consumer problem),也称有限缓冲问题(英语:Bounded-buffer problem),是一个多线程同步问题的经典案例。该问题描述了两个共享固定大小缓冲区的线程——即所谓的“生产者”和“消费者”——在实际运行时会发生的问题。生产者的主要作用是生成一定量的数据放到缓冲区中,然后重复此过程。与此同时,消费者也在缓冲区消耗这些数据。该问题的关键就是要保证生产者不会在缓冲区满时加入数据,消费者也不会在缓冲区中空时消耗数据。这里我们只使用Object类中的wait()、notify()和notifyAll()方法实现。
代码如下(示例):
/**
* 生产者消费者问题——wait()、notifyAll()实现
*/
public class ProducerCustomer {
public static final int WAREHOUSE = 5;
public static void main(String[] args) {
Queue<Integer> queue = new LinkedList<Integer>();
Thread producer1 = new Producer("一号生产者", queue, WAREHOUSE);
Thread producer2 = new Producer("二号生产者", queue, WAREHOUSE);
Thread customer1 = new Customer("消费者1号", queue);
Thread customer2 = new Customer("消费者2号", queue);
Thread customer3 = new Customer("消费者3号", queue);
producer1.start();
producer2.start();
customer1.start();
customer2.start();
customer3.start();
}
}
/**
* 生产者
*/
public class Producer extends Thread {
private Queue<Integer> queue;
String name;
int maxSize;
public Producer(String name, Queue<Integer> queue, int maxSize) {
super(name);
this.name = name;
this.queue = queue;
this.maxSize = maxSize;
}
@Override
public void run() {
while (true) {
synchronized (queue) {
while (queue.size() == maxSize) {
try {
System.out.println("仓库已满," + name + "进入等待");
queue.wait();
} catch (Exception e) {
e.printStackTrace();
}
}
queue.offer(0);
int count = queue.size();
System.out.println("【" + name + "】生产了一个产品,仓库中产品数量为" + count);
queue.notifyAll();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
/**
* 消费者
*/
public class Customer extends Thread {
private Queue<Integer> queue;
String name;
public Customer(String name, Queue<Integer> queue) {
super(name);
this.name = name;
this.queue = queue;
}
@Override
public void run() {
while (true) {
synchronized (queue) {
while (queue.isEmpty()) {
try {
System.out.println("仓库已空," + name + "进入等待");
queue.wait();
} catch (Exception e) {
e.printStackTrace();
}
}
queue.poll();
int count = queue.size();
System.out.println("【" + name + "】" + "消费了一个产品,仓库中产品数量为" + count);
queue.notifyAll();
try {
Thread.sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
运行结果如下:
总结
Good Good Study,Day Day Up!