Java基础教程(140)线程同步之死锁:致命拥抱,深度剖析Java线程死锁,让你的程序永别“尴尬卡死”

一、什么是死锁?

死锁(Deadlock)是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象。若无外力干涉,这些线程都将无法推进下去,整个程序会陷入无限期的“假死”状态。

二、死锁产生的四个必要条件

死锁的发生必须同时满足以下四个条件,缺一不可:

  1. 互斥条件:一个资源每次只能被一个线程使用。
  2. 请求与保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
  3. 不剥夺条件:线程已获得的资源,在未使用完之前,不能被其他线程强行剥夺。
  4. 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。
三、死锁代码示例:经典的“哲学家就餐”问题简化版

我们模拟两位哲学家(线程)和两根筷子(资源)的场景,来完美复现死锁。

public class DeadlockDemo {

    // 两根“筷子”,代表两个需要竞争的资源
    private static final Object chopstickA = new Object();
    private static final Object chopstickB = new Object();

    public static void main(String[] args) {
        // 哲学家A:先拿A,再拿B
        Thread philosopherA = new Thread(() -> {
            synchronized (chopstickA) {
                System.out.println("哲学家A拿到了筷子A。");
                try {
                    Thread.sleep(100); // 模拟思考,增加死锁概率
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (chopstickB) {
                    System.out.println("哲学家A拿到了筷子B,开始吃饭。");
                }
            }
        });

        // 哲学家B:先拿B,再拿A
        Thread philosopherB = new Thread(() -> {
            synchronized (chopstickB) {
                System.out.println("哲学家B拿到了筷子B。");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                synchronized (chopstickA) {
                    System.out.println("哲学家B拿到了筷子A,开始吃饭。");
                }
            }
        });

        philosopherA.start();
        philosopherB.start();
    }
}

运行结果分析:

程序很可能输出以下内容后便永远卡住,不再有任何输出:

哲学家A拿到了筷子A。
哲学家B拿到了筷子B。

死锁过程剖析:

  1. 线程A先锁定了chopstickA
  2. 几乎同时,线程B锁定了chopstickB
  3. 线程A试图去锁定chopstickB,但发现它已被线程B持有,于是线程A阻塞,等待B释放。
  4. 线程B试图去锁定chopstickA,但发现它已被线程A持有,于是线程B也阻塞,等待A释放。
  5. 双方都持有对方所需要的资源且不愿释放,同时又都在请求对方已持有的资源,形成了一个循环等待,死锁就此发生。
四、如何避免和预防死锁?

破解死锁的核心思路就是打破四大必要条件中的至少一个。

  1. 打破循环等待条件(最常用且有效)
    • 顺序资源申请:强制所有线程以统一的顺序申请资源。在上面的例子中,如果两位哲学家都约定先拿编号小的筷子(比如先拿A再拿B),那么死锁就不会发生。因为线程B会先尝试拿A(被A线程持有而阻塞),等A用完释放A和B后,B才能继续执行。
  1. 打破请求与保持条件
    • 一次性申请所有所需资源,在申请不到所有资源时,主动释放已经占有的资源。可以通过Lock.tryLock()等方法实现。
  1. 打破不剥夺条件
    • 允许更高优先级的线程“抢占”低优先级线程已持有的资源。但在Java中synchronized不支持此特性,使用Lock锁可以实现复杂的尝试或超时获取。
  1. 使用检测与恢复机制
    • 这是一种事后补救措施。JDK提供的官方诊断工具,如 jstack,可以自动检测死锁。在命令行运行jstack -l <pid>(pid是Java进程号),工具会清晰地告诉你发现了死锁,并指出哪些线程在等待哪些资源。
总结

死锁是并发编程的顽疾,其根源在于对锁的竞争和管理不当。编写代码时,遵循统一的资源申请顺序是预防死锁最简单有效的黄金法则。同时,善用jstack等工具进行问题定位,并考虑使用java.util.concurrent包中更高级的并发工具(如ReentrantLock及其tryLock方法)来替代简单的synchronized,能够为我们提供更灵活、更安全的并发控制手段,从而构建出更加健壮的多线程应用。

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

值引力

持续创作,多谢支持!

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值