java多线程之线程死锁重点总结

一、死锁概述

定义

当两个或两个以上的线程在执行过程中,因为争夺资源而造成的一种相互等待的状态,由于存在一种环路的锁依赖关系而永远地等待下去,如果没有外部干涉,他们将永远等待下去,此时的这个状态称之为死锁。

举个栗子

类似于生活中两方僵持僵持的情况,如:警察和劫持人质的绑匪,经典的“哲学家就餐问题”
警察对绑匪说:你放了人质,我对你宽大处理
绑匪对警察说:你放了我,我放了人质
在这里插入图片描述

哲学家就餐问题也是一个有意思的案例:
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();

运行结果:这样就不会出现线程阻塞的死锁情况了
在这里插入图片描述

预防死锁方法

如何避免和预防死锁的发生,可以从死锁产生的条件出发考虑,即分别破坏四个条件:

  1. 破坏互斥条件:使资源同时访问而非互斥使用,就没有进程会阻塞在资源上,从而不发生死锁
  2. 破坏请求和保持条件:采用静态分配的方式,静态分配的方式是指进程必须在执行之前就申请需要的全部资源,且直至所要的资源全部得到满足后才开始执行,只要有一个资源得不到分配,也不给这个进程分配其他的资源。
  3. 破坏不剥夺条件:即当某进程获得了部分资源,但得不到其它资源,则释放已占有的资源,但是只适用于内存和处理器资源。
  4. 破坏循环等待条件:给系统的所有资源编号,规定进程请求所需资源的顺序必须按照资源的编号依次进行。

死锁检测

对资源的分配加以适当限制可防止或避免死锁发生,但不利于进程对系统资源的充分共享。

为每个进程和每个资源指定一个唯一的号码,可以使用Jstack命令或者JConsole工具

Jstack命令
jstack用于生成java虚拟机当前时刻的线程快照。线程快照是当前java虚拟机内每一条线程正在执行的方法堆栈的集合,生成线程快照的主要目的是定位线程出现长时间停顿的原因,如线程间死锁、死循环、请求外部资源导致的长时间等待,线程出现停顿的时候通过jstack来查看各个线程的调用堆栈,就可以知道没有响应的线程到底在后台做什么事情,或者等待什么资源。
JConsole工具
Jconsole是JDK自带的监控工具,在JDK/bin目录下可以找到。它用于连接正在运行的本地或者远程的JVM,对运行在Java应用程序的资源消耗和性能进行监控,并画出大量的图表,提供强大的可视化界面。而且本身占用的服务器内存很小,甚至可以说几乎不消耗。

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值