这里写目录标题
一、死锁概述
定义
当两个或两个以上的线程在执行过程中,因为争夺资源而造成的一种相互等待的状态,由于存在一种环路的锁依赖关系而永远地等待下去,如果没有外部干涉,他们将永远等待下去,此时的这个状态称之为死锁。
举个栗子
类似于生活中两方僵持僵持的情况,如:警察和劫持人质的绑匪,经典的“哲学家就餐问题”
警察对绑匪说:你放了人质,我对你宽大处理
绑匪对警察说:你放了我,我放了人质
哲学家就餐问题也是一个有意思的案例:
5个哲学家去吃中餐,坐在一张圆桌旁,他们有5根筷子(而不是5双),并且每两个人中间放一根筷子,哲学家们要么在思考,要么
在进餐,每个人都需要一双筷子才能吃到东西,并在吃完后将筷子放回原处继续思考,有些筷子管理算法能够使每个人都能相对及
时的吃到东西,但有些算法却可能导致一些或者所有哲学家都"饿死",后一种情况将产生死锁:每个人都拥有其他人需要的资源,
同时有等待其他人已经拥有的资源,并且每个人在获取所有需要的资源之前都不会放弃已经拥有的资源。
死锁:每个人都立即抓住自己左边的筷子,然后等待自己右边的筷子空出来,但同时又不放下已经拿到的筷子,形成一种相互等待的状态。
饥饿:哲学家们都同时想吃饭,同时拿起左手边筷子,但是发现右边没有筷子,于是哲学家又同时放下左手边筷子,然后大家发现又有筷子了,又同时开始拿起左手边筷子,又同时放下,然后反复进行。
二、死锁编码实例
这里以警察和劫持人质的绑匪的栗子写代码
首先创建两个类,一个绑匪,一个警察
绑匪类:
/**
* 绑匪
*/
public class Culprit {
public synchronized void say(Police p){
System.out.println("罪犯:你放了我,我放了人质");
p.fun();
}
public synchronized void fun(){
System.out.println("罪犯被放了,罪犯也放了人质");
}
}
警察类:
/**
* 警察
*/
public class Police {
public synchronized void say(Culprit c){
System.out.println("警察:你放了人质,我放了你");
c.fun();
}
public synchronized void fun(){
System.out.println("警察救了人质,但是罪犯跑了");
}
}
}
再创建一个Runnable实现类:写一个能传Culprit和Police对象的构造方法。子线程给p对象的say方法传入对象c,让警察说话,罪犯回应
/**
* Runnable实现类
*/
static class MyThread extends Thread {
private Culprit c;
private Police p;
public MyThread(Culprit c, Police p) {
this.c = c;
this.p = p;
}
@Override
public void run() {
p.say(c);
}
}
最后创建主线程:启动子线程,给c对象的say方法传入p,让罪犯说话,警察回应。
/**
* 测试类,主线程
*/
public static void main(String[] args) {
Culprit c = new Culprit();
Police p = new Police();
new Thread(new MyThread(c, p)).start();
c.say(p);
}
运行结果:电脑运行速度正常,程序卡在这不动,线程阻塞
运行结果:这里是因为电脑太卡,罪犯线程执行完毕警察线程才执行,线程未阻塞,但是这不符合我们的代码逻辑
类似这样的线程死锁在程序开发过程中是尽量要避免发生
三、死锁产生的条件
接下来我们说一说为什么会发生死锁,即死锁的产生条件是什么?
产生机理
当第一个线程拥有A对象锁标记,并等待B对象锁标记,同时第二个线程拥有B对象锁标记,并等待A对象锁标记时,产生死锁。
一个线程可以拥有多个对象的锁标记,当线程阻塞时,不会释放已经拥有的锁标记,由此可能造成死锁。
死锁的四个必要条件
互斥条件
互斥条件:指进程对所分配到的资源进行排它性使用,即在一段时间内某资源只由一个进程占用。如果此时还有其它进程请求资源,则请求者只能等待,直至占有资源的进程用完释放。
请求和保持条件
请求和保持条件:一个线程已经获取到一个锁,再获取另一个锁的过程中,即使获取不到也不会释放已经获得的锁。
不剥夺条件
不可剥夺条件:指进程已获得的资源,在未使用完之前,不能被剥夺,只能在使用完时由自己释放。任何一个线程都无法强制获取别的线程已经占有的锁。
环路等待条件
环路等待条件:指在发生死锁时,必然存在一个进程——资源的环形链,即进程集合{A,B,C,···,Z} 中的A正在等待一个B占用的资源;B正在等待C占用的资源,……,Z正在等待已被A占用的资源。
有时也被称为循环等待条件:线程A拿着线程B的锁,线程B拿着线程A的锁。。
四、死锁的避免与检测
当一组java线程发生死锁的时候,那么这些线程永远不能再使用了,根据线程完成工作的不同,可能会造成应用程序的完全停止,或者某个特定的子系统不能再使用了,或者是性能降低,这个时候恢复应用程序的唯一方式就是中止并重启它,死锁造成的影响很少会立即显现出来,如果一个类发生死锁,并不意味着每次都会发生死锁,而只是表示有可能,当死锁出现的时候,往往是在最糟糕的时候——在高负载的情况下。
避免死锁
警察绑匪案例避免死锁
在根源解决:在任何可能导致锁产生的方法中,不要调用另一个也可能产生锁的方法。
上面的例子修改一下:在调用罪犯的say方法前,还没启动子线程,就不会锁上警察的say方法。等待罪犯说完,警察也回应完,再启动子线程。
c.say(p);
new Thread(new MyThread(c, p)).start();
运行结果:这样就不会出现线程阻塞的死锁情况了
预防死锁方法
如何避免和预防死锁的发生,可以从死锁产生的条件出发考虑,即分别破坏四个条件:
- 破坏互斥条件:使资源同时访问而非互斥使用,就没有进程会阻塞在资源上,从而不发生死锁
- 破坏请求和保持条件:采用静态分配的方式,静态分配的方式是指进程必须在执行之前就申请需要的全部资源,且直至所要的资源全部得到满足后才开始执行,只要有一个资源得不到分配,也不给这个进程分配其他的资源。
- 破坏不剥夺条件:即当某进程获得了部分资源,但得不到其它资源,则释放已占有的资源,但是只适用于内存和处理器资源。
- 破坏循环等待条件:给系统的所有资源编号,规定进程请求所需资源的顺序必须按照资源的编号依次进行。
死锁检测
对资源的分配加以适当限制可防止或避免死锁发生,但不利于进程对系统资源的充分共享。
为每个进程和每个资源指定一个唯一的号码,可以使用Jstack命令或者JConsole工具
Jstack命令
jstack用于生成java虚拟机当前时刻的线程快照。线程快照是当前java虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待,线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。
JConsole工具
Jconsole是JDK自带的监控工具,在JDK/bin目录下可以找到。它用于连接正在运行的本地或者远程的JVM,对运行在Java应用程序的资源消耗和性能进行监控,并画出大量的图表,提供强大的可视化界面。而且本身占用的服务器内存很小,甚至可以说几乎不消耗。