跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
Javajava

Java 多线程死锁:产生原因与解决方案

探讨 Java 多线程死锁。分析死锁产生场景,包括单锁重入、双线程竞争及哲学家就餐模型。阐述死锁发生的四个必要条件:互斥、不可抢占、请求与保持、循环等待。提供两种解决方案:避免嵌套加锁及统一加锁顺序。可通过 JConsole 观察线程阻塞状态进行诊断。

RedisGeek发布于 2026/3/29更新于 2026/5/3028 浏览
Java 多线程死锁:产生原因与解决方案

死锁的产生

我们先从简单的死锁到复杂的问题展开讨论。

首先是一个线程、一把锁,因多次加锁而导致死锁问题。由于 Java 的 synchronized 实现了可重入锁,因此这个死锁问题不存在。这意味着当一个线程拥有一把锁时,可以对该锁进行多次加锁操作,而不会发生死锁问题,前文已详细讨论,此处不再赘述。

接下来是两个线程、两把锁的情况。当两个线程都想获得对方的锁时,就会发生死锁问题。代码如下:

public static void main(String[] args) {
    Object locker1 = new Object();
    Object locker2 = new Object();
    Thread t1 = new Thread(() -> {
        synchronized (locker1) {
            System.out.println("t1 线程获得了 锁 1");
            synchronized (locker2) {
                System.out.println("t1 线程成功获得了 锁 2");
            }
        }
    });
    Thread t2 = new Thread(() -> {
        synchronized (locker2) {
            System.out.println("t2 线程获得了 锁 2");
            synchronized (locker1) {
                System.out.println("t2 线程成功获得了 锁 1");
            }
        }
    });
    t1.start();
    t2.start();
}

代码可能看似能成功执行,但这属于小概率情况。如果发生小概率事件,即死锁状态,程序就会一直卡住。

为什么会执行成功? t1.start() 的速度较快,可能直接获得了两把锁,t2 此时都还没开始执行就结束了。

小概率事件是指什么? 理论上,当 t1 和 t2 线程同时开始执行时,t1 会获得 locker1,t2 获得 locker2。由于 t1 还需要获得 locker2,t2 还需要获得 locker1,但都被对方先拿到了,此时这两个线程就无法继续执行下去,导致两个线程一直处于阻塞状态。

如何查看两个线程阻塞状态? 在 t1 线程加个 sleep,保证 t2 线程获得了 locker2。

public static void main(String[] args) {
    Object locker1 = new Object();
    Object locker2 = new Object();
    Thread t1 = new Thread(() -> {
        synchronized (locker1) {
            System.out.println("t1 线程获得了 锁 1");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
            synchronized (locker2) {
                System.out.println("t1 线程成功获得了 锁 2");
            }
        }
    });
    Thread t2 = new Thread(() -> {
        synchronized (locker2) {
            System.out.println("t2 线程获得了 锁 2");
            synchronized (locker1) {
                System.out.println("t2 线程成功获得了 锁 1");
            }
        }
    });
    t1.start();
    t2.start();
}

使用 JConsole 工具查看,可以看到两个线程进入了 BLOCKED 状态(阻塞状态),并且能看到代码阻塞在第几行。此即为死锁现象。

对于多个线程和多把锁的情况,经典案例是哲学家就餐问题。

图中有 6 位哲学家,每位哲学家的左手和右手两边各有一根筷子。哲学家此时会有两个事件随机发生:拿起左手和右手的筷子吃面,或者思考哲学问题(不吃面)。

试想一个极端情况:如果每一个哲学家此时都想吃面,他们同时拿起左边的筷子,这时候没有一位哲学家拿到一双筷子,并且没有一位哲学家会放弃自己左手的筷子,都在等别人放下的筷子之后拿起来吃面,这时候谁都吃不成。

上面的哲学家可以类比我们的线程,筷子就是锁。虽然这种死锁发生的概率很低,但我们还是要防患于未然。

这种死锁是循环等待导致的,A 等待 B,B 等待 C,C 等待 A,构成一个回路。

死锁发生的原因

首先要回到锁的特性,因为锁是互斥的,一个线程拿到这个锁之后,另一个线程如果想要获得这个锁就必须阻塞等待。

锁是不可抢占的,不可剥夺的。一个线程拿到这个锁之后,除非这个线程解锁释放这个锁,否则其他线程是无法暴力抢占获取的。

请求和保持。这是发生在嵌套的情况下,也就是一个线程拿到锁 1 之后,在不释放锁 1 的前提下,申请获得锁 2,是有可能发生死锁的。换一种说法就是,一个线程在获取到锁 1 的时候,不想放弃这把锁 1,也就是在保持锁 1 的情况下,发出锁 2 的请求。

循环等待。多个线程,多把锁,在等待的过程中构成了循环,A 等待 B,B 也等待 A。

解决死锁的办法

首先回顾第一个和第二个产生死锁的原因,我们直到这个是锁的基本性质引出的,所以我们无力回天,除非你自己写一个锁的设置。

破除嵌套

那我们来看一下第三个问题怎么解决,只要我们避免不要嵌套加锁就可以了,也就是用完锁 1 然后释放掉,最后再申请锁 2 即可。

下面是死锁代码:

Object locker1 = new Object();
Object locker2 = new Object();
Thread t1 = new Thread(() -> {
    synchronized (locker1) {
        System.out.println("t1 线程获得了 锁 1");
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            throw new RuntimeException(e);
        }
        synchronized (locker2) {
            System.out.println("t1 线程成功获得了 锁 2");
        }
    }
});
Thread t2 = new Thread(() -> {
    synchronized (locker2) {
        System.out.println("t2 线程获得了 锁 2");
        synchronized (locker1) {
            System.out.println("t2 线程成功获得了 锁 1");
        }
    }
});
t1.start();
t2.start();

下面的破除嵌套之后的代码:

public static void main(String[] args) {
    Object locker1 = new Object();
    Object locker2 = new Object();
    Thread t1 = new Thread(() -> {
        synchronized (locker1) {
            System.out.println("t1 线程获得了 锁 1");
            try {
                Thread.sleep(1000);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            }
        }
        synchronized (locker2) {
            System.out.println("t1 线程成功获得了 锁 2");
        }
    });
    Thread t2 = new Thread(() -> {
        synchronized (locker2) {
            System.out.println("t2 线程获得了 锁 2");
        }
        synchronized (locker1) {
            System.out.println("t2 线程成功获得了 锁 1");
        }
    });
    t1.start();
    t2.start();
}

运行正常,未发生死锁问题。

破除循环等待

我们可以实现约定好加锁的顺序,就可以破除循环等待了。

例如,我们约定每个线程加锁的时候永远都是获得序号小的锁,然后获得序号大的锁。

目录

  1. 死锁的产生
  2. 死锁发生的原因
  3. 解决死锁的办法
  4. 破除嵌套
  5. 破除循环等待
  • 💰 8折买阿里云服务器限时8折了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • Spring Boot 调用 WebService 接口的两种方法:动态调用与静态调用
  • LLM 大模型技术:入门、应用场景与行业机遇分析
  • 白帽子实战:电商网站常见漏洞挖掘指南
  • 基于Rokid灵珠AI平台的春节全能助手智能体开发实践
  • 英伟达与 GitHub 免费获取大模型 API Key 方法指南
  • RAG 系统优化:应对 7 大挑战提升 LLM 性能
  • Python 编程入门与应用领域分析
  • MCPHost:命令行下通过 MCP 协议与大模型及外部工具交互的工具
  • 人工智能时代:传统产品经理如何转型为 AI 产品经理
  • Unreal Engine 4.27 搭建 AirSim 无人机仿真环境:澳大利亚农村场景
  • LangChain 框架详解与核心应用场景
  • OpenClaw Gateway 部署故障排查:systemctl --user unavailable 问题解析
  • GitHub 教育认证通过后如何领取 Copilot Pro
  • 滑动窗口算法进阶:两道经典题实战
  • MCP 插件配置实战:browser-tools-mcp 集成指南
  • 大模型产品经理转型指南:核心能力与实施路径
  • 大模型训练完整指南:数据、阶段与优化策略详解
  • 算法基础:双指针技巧解决移动零问题
  • C++ 内存管理进阶:从裸指针到智能指针的实战指南
  • Open WebUI 新技术 MCPo:将 MCP 工具转换为 OpenAPI 接口并支持 Ollama

相关免费在线工具

  • Keycode 信息

    查找任何按下的键的javascript键代码、代码、位置和修饰符。 在线工具,Keycode 信息在线工具,online

  • Escape 与 Native 编解码

    JavaScript 字符串转义/反转义;Java 风格 \uXXXX(Native2Ascii)编码与解码。 在线工具,Escape 与 Native 编解码在线工具,online

  • JavaScript / HTML 格式化

    使用 Prettier 在浏览器内格式化 JavaScript 或 HTML 片段。 在线工具,JavaScript / HTML 格式化在线工具,online

  • JavaScript 压缩与混淆

    Terser 压缩、变量名混淆,或 javascript-obfuscator 高强度混淆(体积会增大)。 在线工具,JavaScript 压缩与混淆在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online