什么情况下Java会产生死锁?

引言

这个问题困扰了我很久,感觉知道个大概,但是又模模糊糊。包括怎么发现死锁?死锁是怎么产生的?产生死锁后如何排查等等。今天就一一总结以下以上知识点内容。

死锁的产生

首先,明确概念性问题,什么是 死锁(DeadLock)

所谓死锁是指多个进程因竞争资源而造成的一种僵局(互相等待),若无外力作用,这些进程都将无法向前推进。死锁产生的4个必要条件

  • 互斥条件:进程要求对所分配的资源(如打印机)进行排他性控制,即在一段时间内某 资源仅为一个进程所占有。此时若有其他进程请求该资源,则请求进程只能等待。
  • 不剥夺条件:进程所获得的资源在未使用完毕之前,不能被其他进程强行夺走,即只能 由获得该资源的进程自己来释放(只能是主动释放)。
  • 请求和保持条件:进程已经保持了至少一个资源,但又提出了新的资源请求,而该资源 已被其他进程占有,此时请求进程被阻塞,但对自己已获得的资源保持不放。
  • 循环等待条件:存在一种进程资源的循环等待链,链中每一个进程已获得的资源同时被 链中下一个进程所请求。

面试时的典型回答:

死锁是一种特定的程序状态,在实体之间,由于循环依赖导致彼此一直处于等待之中,没有任何个体可以继续前进。死锁不仅仅是在线程之间会发生,存在资源独占的进程之间同样也可能出现死锁。通常来说,我们大多是聚焦在多线程场景中的死锁,指两个或多个线程之间,由于互相持有对方需要的所,而永久处于阻塞的状态。

诊断死锁

为了更好的理解死锁,以下程序是为了模拟死锁的发生,这是一个基本的死锁程序,这里使用了两个嵌套的synchronized去获取锁。

public class DeadLockSample extends Thread{
    private String first;
    private String second;

    public DeadLockSample(String name, String first, String second) {
        super(name);
        this.first = first;
        this.second = second;
    }

    @Override
    public void run() {
        synchronized (first) {
            System.out.println(this.getName() + " obtained: " + first);
            try {
                Thread.sleep(1000L);
                synchronized (second) {
                    System.out.println(this.getName() + " obtained: " + second);
                }
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        
        String lockA = "lockA";
        String lockB = "lockB";
        DeadLockSample t1 = new DeadLockSample("Thread1", lockA, lockB);
        DeadLockSample t2 = new DeadLockSample("Thread2", lockB, lockA);
        t1.start();
        t2.start();
        t1.join();
        t2.join();
    }
}

下面继续来模拟死锁的定位,可以使用最常见的 jstack ,这里我使用了图形化界面工具 JConsole

从图中可以看出,死锁被检测到了,该程序发生了死锁。并且死锁信息中指明了死锁发生的位置。

在实际应用中,类死锁情况未必有如此清晰的输出,但是总体上可以理解为:

区分线程状态->查看等待目标->对比Monitor等持有状态

所以,理解线程基本状态和并发相关元素是定位问题的关键,然后配合程序调用栈结构,基本就可以定位到具体的问题代码。

如果我们是开发自己的管理工具,需要用更加程序化的方式扫描服务进程、定位死锁,可以考虑使用Java提供的标准管理API, ThreadMXBean ,其直接就提供了 findDeadlockThreads() 方法用于定位死锁。

以下程序为此做出了修改:

public static void main(String[] args) throws InterruptedException {
        ThreadMXBean mxBean = ManagementFactory.getThreadMXBean();
        Runnable dlCheck = new Runnable() {
            @Override
            public void run() {
                long[] threadIdS = mxBean.findDeadlockedThreads();
                if (threadIdS != null) {
                    ThreadInfo[] threadInfos = mxBean.getThreadInfo(threadIdS);
                    System.out.println("检测到死锁:");
                    for (ThreadInfo t : threadInfos) {
                        System.out.println(t.getThreadName() + " " + t.getThreadId());
                    }
                }
            }
        };

        ScheduledExecutorService scheduledExecutorService = Executors.newScheduledThreadPool(1);
        //稍等5秒,然后5秒做一次死锁扫描
        scheduledExecutorService.scheduleAtFixedRate(dlCheck, 5, 5, TimeUnit.SECONDS);

        //死锁样例略
    }

运行程序,控制台就会输出死锁信息:

在实际应用中,就可以据此收集进一步的信息,然后进行预警等后序处理。但是要注意的是,对线程快照本身是一个相对重量级的操作,还是要慎重选择频度和时机。

预防死锁

第一

如果可能的话,尽量避免使用多个锁,并且只有需要时才持有锁。否则,嵌套的 synchronized 或者 lock 非常容易出问题。

第二

如果必须使用多个锁,尽量设计好锁的获取顺序,这个可以参考经典的银行家算法。但一般情况下,可以采取一些简单的辅助手段,比如:

  • 将对象(方法)和锁之间的关系,用图形化的方法表示分别抽取出来。
  • 然后根据对象之间组合、调用的关系对比和组合,考虑所有可能的调用顺序。
  • 按照可能时序合并,发现可能死锁的场景。

第三

使用带超时的方法,为程序带来更多可控性。

Object

public final native void wait(long timeout) throws InterruptedException;

public final void wait(long timeout, int nanos) throws InterruptedException {}

CountDownLatch 类:

public boolean await(long timeout, TimeUnit unit)
        throws InterruptedException {
        return sync.tryAcquireSharedNanos(1, unit.toNanos(timeout));
}

并发Lock的实现,如 ReentrantLock 还支持非阻塞式的获取锁操作:

public boolean tryLock(long timeout, TimeUnit unit)
            throws InterruptedException {
        return sync.tryAcquireNanos(1, unit.toNanos(timeout));
}

这是一个插队行为,并不在乎等待的公平性,如果执行时对象恰好没有被独占,则直接获取锁。有时,我们希望条件允许就尝试插队,不然就按照现有公平性规则等待,一般采用如下方法:

if(lock.tryLock() || lock.tryLock(timeout,unit)){
    //.......
}

除了典型应用中的死锁场景,其实还有一些更令人头疼的死锁,比如类加载过程发生的死锁,尤其是在框架大量使用自定义类加载时,因为往往不是应用本身的代码库中,jstack 等工具也不一定能显示出全部锁的信息。

最后,自旋锁导致的无线循环也是死锁的一种,其他线程因为等待不到具体的信号提示。导致线程一直饥饿。这种情况下可以查看线程CPU使用情况,排查出使用CPU时间片最高的线程,再打出该线程的堆栈信息,排查代码。

基于互斥量的死锁发生往往CPU使用率比较低,实际应用中也可以基于此特点进行排查。

 

  • 3
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值