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

简介: 本文深入讲解多线程编程中的死锁问题,涵盖死锁定义、四个必要条件、经典案例(如转账与哲学家就餐)、代码示例、诊断方法(jstack工具)及预防策略,帮助开发者全面理解并规避死锁,提升并发编程能力。
  • 引言
  • 一、什么是死锁?
  • 二、死锁的四个必要条件
  • 三、经典死锁场景与代码示例
  • 四、如何诊断死锁?
  • 五、死锁的预防与避免策略
  • 六、实际开发中的最佳实践
  • 总结与展望
  • 互动环节

引言

在多线程编程中,死锁(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"
相关文章
|
4月前
|
监控 Java API
JUC系列之《深入剖析LockSupport:Java并发编程的“交警”》
LockSupport是Java并发编程的底层基石,提供park()和unpark()方法实现线程阻塞与精确唤醒。基于“许可证”机制,无需同步块、调用顺序灵活、可精准控制线程,是ReentrantLock、CountDownLatch等高级同步工具的底层支撑,堪称JUC的“手术刀”。
|
4月前
|
Web App开发 安全 Java
并发编程之《彻底搞懂Java线程》
本文系统讲解Java并发编程核心知识,涵盖线程概念、创建方式、线程安全、JUC工具集(线程池、并发集合、同步辅助类)及原子类原理,帮助开发者构建完整的并发知识体系。
|
4月前
|
消息中间件 监控 Java
《聊聊线程池中线程数量》:不多不少,刚刚好的艺术
本文深入探讨Java线程池的核心参数与线程数配置策略,结合CPU密集型与I/O密集型任务特点,提供理论公式与实战示例,帮助开发者科学设定线程数,提升系统性能。
|
4月前
|
Java API 开发者
告别“线程泄露”:《聊聊如何优雅地关闭线程池》
本文深入讲解Java线程池优雅关闭的核心方法与最佳实践,通过shutdown()、awaitTermination()和shutdownNow()的组合使用,确保任务不丢失、线程不泄露,助力构建高可靠并发应用。
|
4月前
|
存储 安全 Java
JUC系列之《深入理解synchronized:Java并发编程的基石 》
本文深入解析Java中synchronized关键字的使用与原理,涵盖其三种用法、底层Monitor机制、锁升级过程及JVM优化,并对比Lock差异,结合volatile应用场景,全面掌握线程安全核心知识。
|
9月前
|
存储 人工智能 安全
赋能数字化转型的创新引擎
阿里云是全球领先的云计算与人工智能科技公司,其强大的技术实力和丰富的解决方案正深刻影响企业运营与竞争力。依托坚实的云计算基础设施,阿里云提供弹性计算、存储与网络服务,满足多样化需求。在AI与大数据领域,机器学习平台PAI及MaxCompute助力智能决策与创新应用。同时,阿里云构建全方位安全防护体系,保障数据隐私,并通过活跃的开发者社区与生态合作推动行业进步。未来,阿里云将持续加大研发投入,优化云原生技术,深化AI与大数据研究,引领数字化转型潮流,共创美好未来。
赋能数字化转型的创新引擎
|
前端开发 Java C++
JUC系列之《CompletableFuture:Java异步编程的终极武器》
本文深入解析Java 8引入的CompletableFuture,对比传统Future的局限,详解其非阻塞回调、链式编排、多任务组合及异常处理等核心功能,结合实战示例展示异步编程的最佳实践,助你构建高效、响应式的Java应用。
|
4月前
|
存储 缓存 Java
【深入浅出】揭秘Java内存模型(JMM):并发编程的基石
本文深入解析Java内存模型(JMM),揭示synchronized与volatile的底层原理,剖析主内存与工作内存、可见性、有序性等核心概念,助你理解并发编程三大难题及Happens-Before、内存屏障等解决方案,掌握多线程编程基石。
|
4月前
|
存储 安全 Java
《Java并发编程的“避坑”利器:ThreadLocal深度解析》
ThreadLocal通过“空间换安全”实现线程变量隔离,为每个线程提供独立副本,避免共享冲突。本文深入解析其原理、ThreadLocalMap机制、内存泄漏风险及remove()最佳实践,助你掌握上下文传递与线程封闭核心技术。
|
4月前
|
存储 缓存 安全
JUC系列之《volatile关键字:穿透Java内存模型的可见性之剑》
本文深入解析Java中的`volatile`关键字,涵盖其核心特性(可见性与有序性)、底层原理(JMM与内存屏障)、典型使用场景(状态标志、单例模式)及局限性(不保证原子性),帮助开发者正确掌握这一轻量级同步工具,避免并发编程误区。