死锁
多线程以及多进程改善了系统资源的利用率并提高了系统 的处理能力。然而,并发执行也带来了新的问题——死锁。所谓死锁是指多个线程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进。
所谓死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象,若无外力作用,它们都将无法推进下去。
- 死锁中的经典例题:哲学家就餐问题
- 相关链接:Java多线程:死锁
造成死锁的必要条件
- 互斥条件:某个资源在某一时间段内只能由一个线程占用。
- 不可抢占条件:线程所得到的资源在未使用完毕前,资源申请者不能强行夺取资源占有者手中的资源。
- 占有且申请条件:线程至少已经占有一个资源,此时又申请新的资源,由于当前新资源被其他线程所占用,该线程阻塞。
- 循环且等待条件:一个线程等待其他线程释放资源,其他线程又在等待另外的线程释放资源,直到最后一个线程等待第一个线程释放资源,这使得所有的线程锁住。
常见的死锁问题
- 1、交叉锁可能引起的的程序死锁问题。
public class TestDemo {
private final Object R1 = new Object();
private final Object R2 = new Object();
//thread1 获取R1之后 切换到thread2
//获取R2,此时R2被thread2使用,阻塞
public void fun1(){
synchronized (R1){
synchronized (R2){
//do something
}
}
}
//thread2 获取R2之后,期望获取R1,
//此时R1被thread1去使用,阻塞,切换到therad1
public void fun2(){
synchronized (R2){
synchronized (R1){
//do something
}
}
}
}
- 2、内存不足,并发请求系统内存,如果当前系统内存不足,也有可能出现死锁的问题。
thread1 10MB 执行都需要30MB的内存 系统可用内存20MB
thread2 20MB 执行都需要30MB的内存 系统可用内存20MB
此时两个线程都会等待其他资源释放内存
- 3、一问一答式的数据交换。
服务器开启某个端口,等待客户端去访问,客户端发起访问请求等到接收服务器端返回的资源,可能由于网络问题服务器端错过了客户端的请求。这样可能导致两边都在等待另一边再次进行资源访问。
- 4、死循环引起的死锁。
哲学家就餐问题
- 死锁代码(有可能出现死锁问题)
class ChopSticks {
protected String name;
public ChopSticks(String name) {
this.name = name;
}
}
class PhilosopherThread extends Thread{
private ChopStick leftChop;
private ChopStick rightChop;
private String name;
public PhilosopherThread(ChopStick leftChop, ChopStick rightChop, String name) {
this.leftChop = leftChop;
this.rightChop = rightChop;
this.name = name;
}
@Override
public void run() {
synchronized (leftChop){
System.out.println(name + " got the chopstick "+leftChop.name);
synchronized (rightChop){
System.out.println(name + " got the chopstick "+rightChop.name);
System.out.println(name + "is eating ");
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(name + "release the chopsticks "+leftChop.name+" and "+rightChop.name);
}
}
}
}
public class TestDemo {
public static void main(String[] args) {
ChopStick chopStick0 = new ChopStick("0");
ChopStick chopStick1 = new ChopStick("1");
ChopStick chopStick2 = new ChopStick("2");
ChopStick chopStick3 = new ChopStick("3");
ChopStick chopStick4 = new ChopStick("4");
new PhilosopherThread(chopStick0, chopStick1, "thread0").start();
new PhilosopherThread(chopStick1, chopStick2, "thread1").start();
new PhilosopherThread(chopStick2, chopStick3, "thread2").start();
new PhilosopherThread(chopStick3, chopStick4, "thread3").start();
new PhilosopherThread(chopStick4, chopStick0, "thread4").start();
}
}
- 如何解决死锁问题的发生?
1、如果当前某位哲学家拿到了左右筷子中的一只,那么此时这个哲学家所在的线程需要阻塞等待,即调用
wait()
方法。
2、如果当前某位哲学家使用完了筷子,我们需要去通知/唤醒等待这只筷子的哲学家,即调用
notify()
和notifyAll()
方法。
3、如果当前某位哲学家获得当前使用的筷子时,需要标识当前筷子不可用。
class ChopSticks {
//key表示筷子的编号,value表示筷子可用状态,false是可用,true是不可用
protected static HashMap<Integer, Boolean> map = new HashMap<>();
static {
map.put(0, false);
map.put(1, false);
map.put(2, false);
map.put(3, false);
map.put(4, false);
}
public synchronized void getChopsticks() {
String currentName = Thread.currentThread().getName();
int leftChop = currentName.charAt(currentName.length() - 1) - '0';
int rightChop = (leftChop + 1) % 5; //leftChop+1;
while (map.get(leftChop) || map.get(rightChop)) {
//有一个为true表示当前这个筷子正在被其他哲学家所使用
//当前线程需要阻塞等待
try {
this.wait(); //释放锁
} catch (InterruptedException e) {
e.printStackTrace();
}
}
map.put(leftChop, true);
map.put(rightChop, true);
System.out.println(Thread.currentThread().getName() + " got the chopsticks " + leftChop +
" and " + rightChop);
}
public synchronized void freeChopsticks() {
String currentName = Thread.currentThread().getName();
int leftChop = currentName.charAt(currentName.length() - 1) - '0';
int rightChop = (leftChop + 1) % 5; //leftChop+1;
map.put(leftChop, false);
map.put(rightChop, false);
this.notifyAll(); //唤醒等待当前这双筷子的哲学家
}
}
public class TestDemo {
public static void main(String[] args) {
ChopSticks chopSticks = new ChopSticks();
for (int i = 0; i < 5; i++) {
new Thread("thread" + String.valueOf(i)) {
@Override
public void run() {
while (true) {
//获得左右两边筷子
chopSticks.getChopsticks();
System.out.println(Thread.currentThread().getName() + " is eating ");
try {
TimeUnit.MILLISECONDS.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
//释放手中的筷子
chopSticks.freeChopsticks();
}
}
}.start();
}
}
}
如何避免死锁的发生
除了规避造成死锁的必要条件中的四种条件之外,避免死锁的发生还有一种特殊的算法,即银行家算法。
银行家算法(Banker’s Algorithm)是一个避免死锁(Deadlock)的著名算法,是由艾兹格·迪杰斯特拉在1965年为T.H.E系统设计的一种避免死锁产生的算法。它以银行借贷系统的分配策略为基础,判断并保证系统的安全运行。
银行家算法就是以银行借贷系统的分配策略为基础,判断并保证系统的安全运行。我们可以把操作系统看作是银行家,操作系统管理的资源相当于银行家管理的资金,进程向操作系统请求分配资源就相当于用户向银行家贷款。
实现方法:
- 为保证资金的安全,银行家规定:
- 1、当一个顾客对资金的最大需求量不超过银行家现有的资金时就可接纳该顾客;
- (即当资源池中剩余的可利用资源 >= 线程还需要的资源时,就可以将可利用资源分配给此线程)
- 2、顾客可以分期贷款,但贷款的总数不能超过最大需求量;
- (线程可以请求分配资源,但是请求的资源总数不能超过资源池中剩余的可利用资源)
- 3、当银行家现有的资金不能满足顾客尚需的贷款数额时,对顾客的贷款可推迟支付,但总能使顾客在有限的时间里得到贷款;
- (当线程池中的资源暂时不满足当前的线程所需时,将此线程先暂时搁置,先将资源分配给能够满足的需求的其他线程,等到线程池中的资源足够满足先前搁置的线程时,在将资源分配给搁置的线程)
- 4、当顾客得到所需的全部资金后,一定能在有限的时间里归还所有的资金。
- (当线程拿到所需要的所有资源,运行结束后,将自身所有的资源放回资源池中)
相关链接:
程序代码:
本算法有3个进程,3类资源。初始可用资源向量为Available{3, 3, 2},然后设置各进程的最大需求矩阵MAX以及分配矩阵Alloction,由此算出需求矩阵Need。然后判断当前系统资源分配是否处于安全状态,否则结束进程。最后,在当前分配资源后的系统安全时,选择一进程,并请求各类所需资源矩阵Request,尝试分配并修改资源分配状况,判断此进程请求是否该分配,进而进入安全算法判断是否能形成一个安全序列,如果有则分配,否则不分配。
public class TestDemo {
//可用资源
private static int[] available = new int[]{3,3,2};
//每个线程最大资源数
private static int[][] max = new int[][]{{8,5,3}, {3,2,2}, {9,0,2},{2,2,2}, {4,3,3}};
//每个线程已分配的资源
private static int[][] allocation = new int[][]{{0,1,1,}, {2,0,0}, {3,0,2}, {2,1,1}, {0,0,2}};
//每个线程需要的资源数
private static int[][] need = new int[][]{{8,4,2}, {1,2,2}, {6,0,0}, {0,1,1}, {4,3,1}};
public static void showData(){
System.out.println("线程编号 最大需求 已分配 还需要");
for(int i=0; i<5; i++){
System.out.print(i+" ");
for(int j=0; j<3; j++){
System.out.print(max[i][j]+" ");//i表示线程号 j表示资源数
}
for(int j=0; j<3; j++){
System.out.print(allocation[i][j]+" ");//i表示线程号 j表示资源数
}
for(int j=0; j<3; j++){
System.out.print(need[i][j]+" ");//i表示线程号 j表示资源数
}
System.out.println();
}
}
//分配资源
public static boolean allocate(int requestNum, int request[]){
//requestNum表示所请求的线程号 request[]当前线程所请求的资源数
if(!(request[0] <= need[requestNum][0] && request[1] <= need[requestNum][1] && request[2] <= need[requestNum][2])){
System.out.println("请求的资源数目超过了当前这个线程还需要的资源数目");
return false;
}
if(!(request[0] <= available[0] && request[1] <= available[1] && request[2] <= available[2])){
System.out.println("目前没有足够的资源分配,必须等待");
return false;
}
//预分配资源给请求的线程
for(int i=0; i<3; i++){
//可分配的资源-请求资源数量
available[i] = available[i] - request[i];
//已经分配的资源allocation + 请求资源数目
allocation[requestNum][i] = allocation[requestNum][i] + request[i];
//还需要资源数need - 请求的资源数
need[requestNum][i] = need[requestNum][i] - request[i];
}
//进行安全性检查,true表示剩余资源能够满足其余线程的资源请求,false表示无法满足
boolean flag = checkSafe();
if(flag){
System.out.println("能够安全分配");
return true;
}else{
//不能够通过安全性检查,撤销之前预分配的资源
System.out.println("不能够安全分配");
for(int i=0; i<3; i++){
//可分配的资源+请求资源数量
available[i] = available[i] + request[i];
//已经分配的资源allocation - 请求资源数目
allocation[requestNum][i] = allocation[requestNum][i] - request[i];
//还需要资源数need + 请求的资源数
need[requestNum][i] = need[requestNum][i] + request[i];
}
return false;
}
}
public static boolean checkSafe(){
/**
* 循环遍历其余线程,查看可用资源是否能够满足其余线程的资源请求,
* 如果满足则进行下一次的遍历,如果不满足直接判断下一个线程
*
* t1 error ok
* t2 ok
* t3 error ok
* t4 error ok
* t5 ok
*
*/
int i = 0;
boolean[] finish = new boolean[5];
while(i < 5){
if(finish[i] == false && need[i][0] <= available[0] && need[i][1] <= available[1] && need[i][2] <= available[2]){
System.out.println("分配成功的线程为:"+i);
//执行成功之后还需要将所有资源释放
for(int j=0; j<3; j++){
available[j] = available[j] + allocation[i][j];
}
finish[i] = true;//表示当前线程已经执行完
i = 0;
}else{
i++;
}
}
//while循环结束之后,所有finish标识都为true,表示所有线程都已经执行完
for(int m=0; m<5; m++){
if(finish[m] == false){
return false;
}
}
return true;
}
public static void main(String[] args) {
showData();
System.out.println("当前系统可用资源:");
for(int i=0; i<3; i++){
System.out.print(available[i] + " ");
}
System.out.println();
//请求线程资源存放的数组
int[] request = new int[3];
int requestNum;
String source[] = new String[]{"A", "B", "C"};
Scanner s = new Scanner(System.in);
String choice = new String();
while(true){
System.out.println("请输入要请求的线程编号:");
requestNum = s.nextInt();
System.out.println("请输入要请求的资源数目:");
for(int i=0; i<3; i++){
System.out.println(source[i]+"资源的数目:");
request[i] = s.nextInt();
}
//分配资源
allocate(requestNum, request);
System.out.println("是否再次请求分配(y/n)");
choice = s.next();
if(choice.equals("n")){
break;
}
}
}
}
银行家算法的核心思想:
- 系统在资源分配的时候先进性预分配,计算资源分配的安全性,如果说当前资源分配会导致系统进入不安全状态,系统将预分配的资源撤回,反之,系统会将真正的资源分配给进程。
死锁的预防:打破产生死锁的四个必要条件其中一个或者多个。