JUC系列之《死锁:并发编程中的「完美风暴」与破解之道》

  • 引言
  • 一、什么是死锁?
  • 二、死锁的四个必要条件
  • 三、经典死锁场景与代码示例
  • 四、如何诊断死锁?
  • 五、死锁的预防与避免策略
  • 六、实际开发中的最佳实践
  • 总结与展望
  • 互动环节

引言

在多线程编程中,死锁(Deadlock)就像一场交通瘫痪:四辆车同时到达十字路口,每辆车都在等待其他车先通过,结果谁都动不了 。

这种"互相等待"的局面在并发系统中同样致命——线程永久阻塞,程序停滞不前,系统吞吐量降为零。理解死锁、学会避免死锁,是每个Java开发者迈向高级阶段的必修课。本文将带你彻底攻克这个难题!


一、什么是死锁?

死锁是指两个或两个以上的线程在执行过程中,因争夺资源而造成的一种互相等待的现象。若无外力干涉,这些线程都将无法推进下去。

简单比喻

假设你有两个小朋友:小明和小红 。

小明拿着玩具汽车,但想要小红的玩具熊

小红拿着玩具熊,但想要小明的玩具汽车

两人都不愿意先放下自己手中的玩具,于是就僵持住了...这就是死锁!

二、死锁的四个必要条件

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

  1. 互斥条件:资源不能被共享,只能由一个线程使用
  2. 占有且等待:线程已经持有至少一个资源,但又等待获取其他线程持有的资源
  3. 不可抢占:资源只能由持有它的线程主动释放,不能被强制抢占
  4. 循环等待:存在一个线程-资源的环形链:T1等待T2占有的资源,T2等待T3占有的资源,...,Tn等待T1占有的资源

打破其中任意一个条件,就能预防死锁!

三、经典死锁场景与代码示例

1. 转账死锁案例

这是最经典的死锁场景:两个人同时向对方转账。

public class BankTransferDeadlock {
    static class Account {
        private String name;
        private int balance;
        
        public Account(String name, int balance) {
            this.name = name;
            this.balance = balance;
        }
        
        void debit(int amount) {
            balance -= amount;
        }
        
        void credit(int amount) {
            balance += amount;
        }
    }
    
    // 转账方法 - 有死锁风险!
    public static void transfer(Account from, Account to, int amount) {
        synchronized(from) {
            System.out.println(Thread.currentThread().getName() 
                + " 锁住了 " + from.name);
            
            // 模拟一些操作,增加死锁发生概率
            try { Thread.sleep(100); } catch (InterruptedException e) {}
            
            synchronized(to) {
                System.out.println(Thread.currentThread().getName() 
                    + " 锁住了 " + to.name);
                
                if (from.balance >= amount) {
                    from.debit(amount);
                    to.credit(amount);
                    System.out.println("转账成功");
                } else {
                    System.out.println("余额不足");
                }
            }
        }
    }
    
    public static void main(String[] args) {
        Account accountA = new Account("张三", 1000);
        Account accountB = new Account("李四", 1000);
        
        // 线程1:张三向李四转账100
        Thread thread1 = new Thread(() -> transfer(accountA, accountB, 100));
        
        // 线程2:李四向张三转账100
        Thread thread2 = new Thread(() -> transfer(accountB, accountA, 100));
        
        thread1.start();
        thread2.start();
        
        try {
            thread1.join();
            thread2.join();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        
        System.out.println("张三余额: " + accountA.balance);
        System.out.println("李四余额: " + accountB.balance);
    }
}

运行结果可能

Thread-0 锁住了 张三
Thread-1 锁住了 李四
(然后程序就卡在这里了...)

2. 哲学家就餐问题

另一个经典死锁案例:五位哲学家围坐圆桌,每人面前有一碗饭,每两人之间有一根筷子。哲学家需要两根筷子才能吃饭。

public class DiningPhilosophers {
    static class Philosopher implements Runnable {
        private final Object leftChopstick;
        private final Object rightChopstick;
        
        public Philosopher(Object left, Object right) {
            this.leftChopstick = left;
            this.rightChopstick = right;
        }
        
        @Override
        public void run() {
            try {
                while (true) {
                    // 思考
                    doAction("思考中...");
                    
                    synchronized(leftChopstick) {
                        doAction("拿起左边筷子");
                        synchronized(rightChopstick) {
                            // 吃饭
                            doAction("拿起右边筷子 - 开始吃饭");
                            doAction("放下右边筷子");
                        }
                        doAction("放下左边筷子 - 回归思考");
                    }
                }
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
        
        private void doAction(String action) throws InterruptedException {
            System.out.println(Thread.currentThread().getName() + " " + action);
            Thread.sleep((long) (Math.random() * 10));
        }
    }
    
    public static void main(String[] args) {
        final int PHILOSOPHERS_COUNT = 5;
        Object[] chopsticks = new Object[PHILOSOPHERS_COUNT];
        
        for (int i = 0; i < PHILOSOPHERS_COUNT; i++) {
            chopsticks[i] = new Object();
        }
        
        for (int i = 0; i < PHILOSOPHERS_COUNT; i++) {
            Object leftChopstick = chopsticks[i];
            Object rightChopstick = chopsticks[(i + 1) % PHILOSOPHERS_COUNT];
            
            // 避免死锁的调整:让最后一个哲学家先拿右边筷子
            if (i == PHILOSOPHERS_COUNT - 1) {
                Thread philosopher = new Thread(
                    new Philosopher(rightChopstick, leftChopstick),
                    "哲学家-" + (i + 1)
                );
                philosopher.start();
            } else {
                Thread philosopher = new Thread(
                    new Philosopher(leftChopstick, rightChopstick),
                    "哲学家-" + (i + 1)
                );
                philosopher.start();
            }
        }
    }
}

四、如何诊断死锁?

当程序出现"卡死"现象时,如何确认是死锁?

1. 使用jstack工具

# 1. 找到Java进程ID
jps

# 2. 生成线程转储信息
jstack <pid>

# 或者直接使用jcmd
jcmd <pid> Thread.print

查看输出中的死锁信息

Found one Java-level deadlock:
=============================
"Thread-1":
  waiting to lock monitor 0x00007faa1c007d80 (object 0x000000076ab66d40, a java.lang.Object),
  which is held by "Thread-0"
"Thread-0":
  waiting to lock monitor 0x00007faa1c007a80 (object 0x000000076ab66d50, a java.lang.Object),
  which is held by "Thread-1"
评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

一枚后端工程狮

你的鼓励将是我创作的最大动力

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

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

打赏作者

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

抵扣说明:

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

余额充值