
多线程和多进程提高了系统资源的利用率,并提高了系统的处理能力. 但是,并发执行还会带来新的问题僵局.
所谓的死锁是指由多个线程竞争资源而导致的死锁(彼此等待). 没有外力,这些进程将无法前进.

死锁必须满足以下四个条件. 只要系统出现死锁,就必须建立这些条件,并且只要不满足上述条件之一,就不会发生死锁.
该进程需要对分配的资源(例如打印机)进行排他控制,也就是说,某些资源仅在一段时间内被某个进程占用. 此时,如果另一个进程请求资源,则请求进程只能等待.
一个进程获取的资源在不耗尽之前不能被其他进程强行带走,也就是说,它们只能由自己获取资源的进程释放(只能主动释放).
该进程保留了至少一个资源,但发出了新的资源请求,并且该资源已被其他进程占用. 这时,请求进程被阻止,但保留了已获取的资源.
过程资源有一个循环的等待链. 链中每个进程同时请求下一个进程获取的资源. 也就是说,存在一个等待进程集{P1,P2,…,pn},其中等待Pi的资源被P(i +1)(i = 0、1,…,n-1)占用,并且如图所示,等待Pn占用的Pn资源.

package com.lock; /** * @Auther: lanhaifeng * @Date: 2019/11/21 0021 08:58 * @Description: 死锁测试类 * @statement: */ public class DeadLock implements Runnable{ private int flag;//决定线程走向的标记 private static Object obj1 = new Object();//锁对象1 private static Object obj2 = new Object();//锁对象2 public DeadLock(int flag){ this.flag = flag; } public void run() { if(flag == 1){ //线程1执行代码: synchronized (obj1){ System.out.println(Thread.currentThread().getName()+"已获取到资源obj1,请求obj2"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (obj2){ System.out.println(Thread.currentThread().getName()+"已经获取到obj1和obj2!"); } } } else { //线程2执行代码 synchronized (obj2){ System.out.println(Thread.currentThread().getName()+"已获取到资源obj2,请求obj1"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (obj1){ System.out.println(Thread.currentThread().getName()+"已经获取到obj1和obj2!"); } } } } //测试死锁 public static void main(String[] args){ //1.创建两个DeadLockRunnable实例:flag = 1;flag = 2 DeadLock deadLock1 = new DeadLock(1); DeadLock deadLock2 = new DeadLock(2); //2.创建两个线程执行两个DeadLockRunnable实例 Thread thread1 = new Thread(deadLock1,"线程1"); Thread thread2 = new Thread(deadLock2,"线程2"); thread1.start(); thread2.start(); } }
执行效果如下: 线程1获取object1请求obeject2,线程2获取obeict2请求object1,从而引起了相互等待的现象. 指示发生死锁

l防止死锁: 通过设置某些限制性条件来破坏四个必要的死锁条件中的一个或几个,以防止死锁的发生.
l避免死锁: 在动态分配资源期间,请使用一种方法来防止系统进入不安全状态,从而避免死锁.
l死锁检测: 允许系统在运行期间死锁,但可以设置检测机制以及时检测死锁并采取适当措施清除死锁.
l死锁释放: 检测到死锁时,将采取适当的措施将进程从死锁状态释放.

防止死锁是试图至少破坏死锁并严格防止死锁发生的四个必要条件之一.
“互斥”条件不能被打破. 因此,在防止死锁中主要是破坏其他几个必要条件,而不是破坏“互斥”条件.
销毁“拥有并等待”条件,这意味着在获取某些资源后,不允许该流程申请其他资源. 那是找到一种方法来防止该进程在申请其他资源时占用资源.
l方法1: 一次分配资源,即在创建流程时,要求申请所有必需的资源,系统可能满足其所有要求,或者什么都不给.
方法2: 在申请新资源之前,要求每个进程释放其拥有的资源. 这样,当一个进程需要资源S时,它必须首先释放它先前占用的资源R,然后它可以申请S,即使它可能很快会再次使用资源R.
破坏“不可抢占”条件是为了抢夺资源.
l方法1: 如果拒绝了某个进程占用某些资源,则该进程必须释放其最初占用的资源,并在必要时再次请求这些资源和其他资源.
方法2: 如果一个进程请求当前由另一个进程占用的资源,则操作系统可以抢占另一个进程并要求其释放资源. 只有在两个进程的优先级不同的情况下,第二种方法才能防止死锁.
破坏“循环等待”条件的一种方法是对系统中的所有资源进行统一编号. 该过程可以随时提交资源请求,但是所有应用程序都必须按照资源编号的顺序(升序)提交. 这样做将确保系统不会死锁.
避免死锁并不能严格限制死锁的必要条件的存在,因为即使存在死锁的必要条件,也不一定会发生死锁.
算法实现步骤如下:
l必须对所有资源进行统一编号,例如打印机1,传真机2,磁盘3等.
l必须一次申请类似的资源. 例如,打印机和传真机通常是同一台机器,必须同时应用
l必须按顺序应用不同类型的资源
例如: 有两个进程P1和P2,有两个资源R1和R2
P1请求资源: R1,R2

P2请求资源: R1,R2
这会破坏循环条件并避免死锁.
银行家算法(Banker's Algorithm)是一种避免死锁(Deadlock)的著名算法. 它是Izzie Dijkstra在1965年为T.H.E系统设计的一种避免死锁的算法. 它是基于银行贷款系统的分配策略来确定并确保系统的安全运行. 流程图如下:

银行家算法的基本思想是在分配资源之前确定系统是否安全;如果是这样,请仅分配. 这是避免死锁的最有代表性的算法.
假设我要请求REQUEST [i],那么银行家的算法将根据以下规则进行判断.
1)如果请求[i] <=需要[i,j],则转到(2);否则,将发生错误.
2)如果请求[i] <=可用[i]java多线程避免死锁,则转到(3);否则,请等待.
3)系统尝试分配资源并修改相关数据:
可用[i]-=请求[i]; //可用资源数量-请求的资源数量
分配[i] + =请求[i]; //分配的资源数量+请求的资源数量
需要[i]-=请求[i]; //所需资源数-请求资源数
4)系统执行安全检查. 如果安全,则建立分配;否则,探索性分配无效,系统返回到其原始状态,然后过程等待.
当多个线程需要相同的锁,但以不同的顺序锁定它们时,很容易发生死锁.
例如,以下两个线程将死锁:
线程1:

lock A (when C locked) lock B (when C locked) wait for C
线程2:
wait for A wait for B lock C (when A locked)
如果可以确保所有线程以相同顺序获取锁,则不会发生死锁. 例如,以下两个线程不会死锁
线程1:
lock A lock B lock C
线程2:
wait for A wait for B wait for C
顺序锁定是一种有效的防止死锁的机制. 但是,此方法需要事先知道所有可能使用的锁,但是有时它是不可预测的,因此该方法仅适用于特定情况.
限时锁定是指线程尝试获取锁定并添加超时期限时. 如果超过了此时间,则将放弃对锁的请求,并将所有获取的锁回滚并释放,然后等待随机的时间段,然后重试.
下面是一个示例,该示例显示了一个场景,其中两个线程尝试以不同的顺序获取相同的两个锁,先退后再在发生超时后重试:
Thread 1 locks A Thread 2 locks B Thread 1 attempts to lock B but is blocked Thread 2 attempts to lock A but is blocked Thread 1’s lock attempt on B times out Thread 1 backs up and releases A as well Thread 1 waits randomly (e.g. 257 millis) before retrying. Thread 2’s lock attempt on A times out Thread 2 backs up and releases B as well Thread 2 waits randomly (e.g. 43 millis) before retrying.
在上面的示例中,线程2重试以比线程1早200毫秒锁定,因此它可以首先成功地获得两个锁定. 此时,线程1尝试获取锁A并处于等待状态. 当线程2结束时,线程1也可以成功获得这两个锁.
此方法有两个缺点:
1)当线程数较少时,此方法可以避免死锁. 但是,当线程过多时,这些线程的锁定时间相同的可能性会更高,这可能导致超时后无休止的重试循环.
2)无法在Java中为同步块设置超时时间. 您需要创建自定义锁,或使用Java 5中java.util.concurrent包下的工具.

防止和避免死锁具有较大的系统开销,并且无法充分利用资源. 更好的方法不是采取任何限制性措施,而是提供检测和释放死锁的方法. 这是死锁检测和恢复.
死锁检测数据结构:
l E是一个现有的资源向量(existing resource vector),即代码中每个现有资源的总数
l A是一个可用资源向量(available resource vector),则Ai代表当前可用资源的数量(即,没有分配资源)
l C是当前分配矩阵(当前分配矩阵),C的第i行代表Pi当前持有的每种资源的资源数量.
l R是请求矩阵(request matrix),R的每一行代表P所需的资源数量.

死锁检测步骤:
1)找到一个没有结束标记的过程Pi,其R矩阵的第i行的向量小于或等于A.
2)如果找到了这样的过程,请执行该过程,然后将C矩阵的第i行向量添加到A,标记该过程,然后转到步骤1
3)如果没有这样的过程,则算法终止
4)在算法的最后,所有未标记的进程都是死锁进程.
使用抢占来恢复.
暂时将资源从其当前进程转移到另一个进程.
这种方法可能需要人工干预,主要方法的可行性取决于资源本身的特征.
使用回滚恢复
定期备份进程状态. 如果发现进程死锁,则会根据备份将进程重置为较早的状态,并且尚未获得所需的资源,然后将这些资源分配给其他死锁进程.
通过杀死进程进行恢复
最直接的方法是杀死一个或多个进程.
尽可能确保杀死过程从头开始java多线程避免死锁,没有副作用.
本文来自电脑杂谈,转载请注明本文网址:
http://www.pc-fly.com/a/jisuanjixue/article-211123-1.html
真的很棒创作歌手黄子韬