什么是死锁:
死锁是指两个或两个以上线程在执行过程中,由于竞争资源而造成的阻塞问题,若无外力作用下,他们将无法推荐下去,此时系统处于死锁状态。
死锁的危害:
- 导致程序得不到正确的运行结果:因为程序产生死锁,发生阻塞,不会继续向下执行。
- 浪费资源:产生死锁,最少有两个线程会发生阻塞。
- 产生新的死锁:产生死锁的线程会一直占有锁资源,会导致其它尝试获取该锁的线程也发生死锁,产生多米诺效应。
死锁产生的原因:
- 因竞争资源产生死锁;
- 进程顺序推进不当产生死锁;
出现死锁的必要条件:
- 互斥条件: 资源每次只能是一个线程使用。 ==》 资源
- 请求与保持条件:一个线程因请求资源而阻塞时,对已获取的资源保持不释放。==》线程
- 不可剥夺条件:线程已获取的资源,在未使用之前,不能强行剥夺,只能在使用完时由自己释放。
- 循环等待条件:若干线程之间形成一种头尾相连接的循环等待资源关系。
死锁的的预防或解除:
解决死锁的途径:预防(事前),避免(事中),检测和恢复(事后,已发生)
预防死锁(破坏产生死锁的四个必要条件):
- 资源一次性分配(破坏请求与保持条件);
- 可剥夺资源:在线程未满足条件时,释放掉已占有的资源。
- 资源有序分配:系统给每类资源赋予一个编号,每个线程按编号递增的顺序请求资源,释放则相反(请求123 释放321 )
避免死锁(银行家算法):后文具体介绍
允许线程动态的申请资源,系统在资源分配之前先计算资源分配的安全性,若此次分配不会导致系统进入不安全状态,则给线程分配该资源,负责线程等待;
检测与解除死锁:
当线程发现进入了死锁,立即从死锁状态解除掉。
- 采用方式:剥夺资源,从其它线程剥夺足够多的资源给死锁线程,以免除死锁状态的线程。
引起死锁的案例:
1,生产者,消费者使用不当会产生死锁(wait,notify,notifyAll)
2,多线程获取多把锁:
-
产生死锁
lock1.lock();
lock2.lock();
//todo
lock1.unlock()
lock2.unlock() -
不产生死锁:先请求后释放
lock1.lock();
lock2.lock();
//todo
lock2.unlock()
lock1.unlock()
3,哲学家就餐问题:(产生死锁原因,每个哲学家都获得了一只筷子,卡死在那个地方)。
- 圆桌上有五位哲学家,每两位中间有一双筷子。
- 每个哲学家有两件事要做:思考,吃饭(哲学家必须同时拿到两个筷子才能吃饭)
- 哲学家之间并不知道对方何时要吃饭,何时要思考,不能协商制定吃饭,思考策略。
- 制定一个拿筷子的策略,使得哲学家不会因为拿筷子而出现死锁,乐观锁。
解决方案:
- 添加一个服务生,只有服务生同意才能才能拿起筷子,服务生负责避免死锁发生。
- 每个哲学家必须确定左右手筷子都可用时,才能同时拿起两只筷子进餐,吃完之后同时放下两只筷子。
- 规定每个哲学家拿筷子时必须拿序号小那只,这样最后一位未拿到筷子的哲学家只剩下序号大的那只筷子不能拿起,剩下的这支筷子就可以被其它哲学家使用,避免了死锁,这种情况不能很好的利用资源。
- 其它方案请参考此博客:哲学家就餐问题解决方案整合
代码实现:采用第二种方案
package cn.edu.sdust.Philosopher;
/*每个哲学家相当于一个线程*/
class Philosopher extends Thread{
private String name;
private Fork fork;
public Philosopher(String name,Fork fork){
super(name); //设置线程名
this.name=name;
this.fork=fork;
}
public void run(){
while(true){
thinking();
fork.takeFork();
eating();
fork.putFork();
}
}
public void eating(){
System.out.println("I am Eating:"+name);
try {
sleep(1000);//模拟吃饭,占用一段时间资源
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
public void thinking(){
System.out.println("I am Thinking:"+name);
try {
sleep(1000);//模拟思考
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
class Fork{
/*5只筷子,初始为都未被用*/
private boolean[] used={false,false,false,false,false,false};
/*只有当左右手的筷子都未被使用时,才允许获取筷子,且必须同时获取左右手筷子*/
public synchronized void takeFork(){
String name = Thread.currentThread().getName();
int i = Integer.parseInt(name);
while(used[i]||used[(i+1)%5]){ //只要有一个被使用,那么就等待
try {
wait();//如果左右手有一只正被使用,等待
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
used[i ]= true;
used[(i+1)%5]=true;
}
/*必须同时释放左右手的筷子*/
public synchronized void putFork(){
String name = Thread.currentThread().getName();
int i = Integer.parseInt(name);
used[i ]= false;
used[(i+1)%5]=false;
notifyAll();//唤醒其他线程
}
}
//测试
public class ThreadTest {
public static void main(String []args){
Fork fork = new Fork();
new Philosopher("0",fork).start();
new Philosopher("1",fork).start();
new Philosopher("2",fork).start();
new Philosopher("3",fork).start();
new Philosopher("4",fork).start();
}
}
实际定位死锁问题的思路:
jdk提供的JAVA命令在哪里?
在jdk的bin目录下,当我们需要执行java命令时:打开DOS命令行(CMD)进入到jdk的bin目录下,然后就可以运行JAVA命令。
- 首先需要确定java进程是否发生死锁(程序停滞)
- 打开 jvisualvm 工具,专门分析JVMCPU,内存使用情况,以及线程的运行信息查看当前java进程各个线程运行的状态(颜色)
- 通过jvisualvm的线程dump或者jstack命令,把当前java进程所有线程的调用堆栈信息打印出来;
- 分析main线程和子线程有没有关键短语: waiting for(资源地址) / waiting to lock(资源地址)
- 看线程函数调用栈,定位到源码上,具体问题具体分析。
参考博客:
银行家算法:
银行家算法是避免死锁的一种重要方法。操作系统按照银行家制定的规则为进程(线程)分配资源,当进程首次申请资源时,要测试该进程对资源的最大需求量,如果系统现存资源可以满足它的最大需求量,则按当前的申请量分配资源,否则就延迟分配。 当进程在执行中继续申请资源时,先测试该进程已占用的资源数与本次申请的资源数之和是否超过了该进程对资源的最大需求量。若超过则拒绝分配资源,若没有超过则在测试系统现存的资源能否满足该进程尚需的最大资源量,若能满足则按当前的申请量分配资源,否则也要推迟分配。
算法数据结构:
- 可利用资源向量Available(资源池): 是个含有m个元素的数组,其中的每个元素代表一类可利用的资源数目。如Available[j]=k,则表示系统中出现Rj类资源k个。
- 最大需求矩阵Max: 这是一个n*m的矩阵,它定义了系统中n个进程中每一个进程对m类资源的最大需求。如Max[i,j]=k ,则表示进程i所需Rj类资源的最大数目为k;
- 分配矩阵Allocation: n*m矩阵,它定义了系统中每一类资源当前已分配给每一进程的资源数。
-
需求矩阵Need: n*m矩阵,用以表示每一个进程尚需的各类资源数。
算法实现:
我们从p0开始向资源池请求Need所需的各类资源,如果满足则将线程能否完成置为true(Available-Need),并且将Max的所有资源加入到资源池中(Available+Max)。若p0不满足置为false。接着执行p1->p2。
此过程是循环执行的,但是不能无限制执行(万一有一线程资源永远不能被满足) 最多执行n次(Pn)。
这样,我们根据线程先后满足资源的次数,若所有线程finish状态为true,则找到一个安全执行序列。