
网罗开发 (小红书、快手、视频号同名)
大家好,我是 展菲,目前在上市企业从事人工智能项目研发管理工作,平时热衷于分享各种编程领域的软硬技能知识以及前沿技术,包括iOS、前端、Harmony OS、Java、Python等方向。在移动端开发、鸿蒙开发、物联网、嵌入式、云原生、开源等领域有深厚造诣。
图书作者:《ESP32-C3 物联网工程开发实战》
图书作者:《SwiftUI 入门,进阶与实战》
超级个体:COC上海社区主理人
特约讲师:大学讲师,谷歌亚马逊分享嘉宾
科技博主:华为HDE/HDG
我的博客内容涵盖广泛,主要分享技术教程、Bug解决方案、开发工具使用、前沿科技资讯、产品评测与使用体验 。我特别关注云服务产品评测、AI 产品对比、开发板性能测试以及技术报告,同时也会提供产品优缺点分析、横向对比,并分享技术沙龙与行业大会的参会体验。我的目标是为读者提供有深度、有实用价值的技术洞察与分析。
展菲:您的前沿技术领航员
👋 大家好,我是展菲!
📱 全网搜索"展菲",即可纵览我在各大平台的知识足迹。
📣 公众号"Swift社区",每周定时推送干货满满的技术长文,从新兴框架的剖析到运维实战的复盘,助您技术进阶之路畅通无阻。
💬 微信端添加好友"fzhanfei",与我直接交流,不管是项目瓶颈的求助,还是行业趋势的探讨,随时畅所欲言。
📅 最新动态:2025 年 3 月 17 日
快来加入技术社区,一起挖掘技术的无限潜能,携手迈向数字化新征程!
文章目录
-
- 前言
- 什么是死锁?用最直观的方式理解
-
- [1. 一个最经典的死锁 Demo(可直接运行)](#1. 一个最经典的死锁 Demo(可直接运行))
- [2. 这段代码到底发生了什么?](#2. 这段代码到底发生了什么?)
- 真实业务中,死锁通常长什么样?
-
- [常见场景 1:订单 + 库存](#常见场景 1:订单 + 库存)
- [常见场景 2:缓存 + 数据库](#常见场景 2:缓存 + 数据库)
- 解决方案一:按固定顺序加锁(最重要)
-
- [1. 原则很简单](#1. 原则很简单)
- [2. 改造上面的死锁代码](#2. 改造上面的死锁代码)
- [3. 为什么这种方式最靠谱?](#3. 为什么这种方式最靠谱?)
- [解决方案二:使用 tryLock,避免无限等待](#解决方案二:使用 tryLock,避免无限等待)
-
- [1. 核心思想](#1. 核心思想)
- [2. 示例代码(可运行)](#2. 示例代码(可运行))
- [3. tryLock 的真实价值](#3. tryLock 的真实价值)
- 解决方案三:使用并发包,减少"自己加锁"
-
- [1. 示例:用 ConcurrentHashMap 替代 synchronized Map](#1. 示例:用 ConcurrentHashMap 替代 synchronized Map)
- [2. 为什么并发包更安全?](#2. 为什么并发包更安全?)
- 一些非常实用的经验总结
- 总结
前言
只要你写过稍微复杂一点的并发代码,大概率都遇到过这种情况:
- 程序不报错
- CPU 占用不高
- 日志也不动
- 但是服务就是「卡住了」
十有八九,你遇到的是 死锁。
死锁本身并不神秘,本质只有一句话:
多个线程互相等待对方持有的锁资源,谁也不肯先放手。
但真正的问题在于------
死锁往往不是一行代码的问题,而是整体设计的问题。
下面我们一步一步来拆。
什么是死锁?用最直观的方式理解
先别急着上概念,我们直接看代码。
1. 一个最经典的死锁 Demo(可直接运行)
下面这段代码,几乎是所有死锁案例的「祖师爷」。
java
public class DeadLockDemo {
private static final Object lockA = new Object();
private static final Object lockB = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (lockA) {
System.out.println("Thread-1 拿到了 lockA");
sleep(100);
synchronized (lockB) {
System.out.println("Thread-1 拿到了 lockB");
}
}
});
Thread t2 = new Thread(() -> {
synchronized (lockB) {
System.out.println("Thread-2 拿到了 lockB");
sleep(100);
synchronized (lockA) {
System.out.println("Thread-2 拿到了 lockA");
}
}
});
t1.start();
t2.start();
}
private static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException ignored) {}
}
}
2. 这段代码到底发生了什么?
执行流程非常容易脑补:
-
Thread-1
- 先拿
lockA - 再等
lockB
- 先拿
-
Thread-2
- 先拿
lockB - 再等
lockA
- 先拿
结果就是:
- Thread-1 等 Thread-2 放
lockB - Thread-2 等 Thread-1 放
lockA - 双方无限期等待
程序不会崩,也不会报错,但它已经「死」了。
真实业务中,死锁通常长什么样?
死锁在真实项目中,很少像上面这么"工整",但模式几乎一模一样。
常见场景 1:订单 + 库存
text
线程 A:锁订单 → 锁库存
线程 B:锁库存 → 锁订单
只要顺序一反,风险立刻出现。
常见场景 2:缓存 + 数据库
- 一个线程:先查缓存,没命中再锁 DB
- 另一个线程:先更新 DB,再回写缓存
在高并发下,非常容易卡死。
重点不是你用了几把锁,而是:锁的顺序有没有被统一设计过。
解决方案一:按固定顺序加锁(最重要)
这是成本最低、收益最高的一种方式。
1. 原则很简单
只要多个线程获取多个锁,顺序必须完全一致。
2. 改造上面的死锁代码
java
public class OrderedLockDemo {
private static final Object lockA = new Object();
private static final Object lockB = new Object();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
synchronized (lockA) {
System.out.println("Thread-1 拿到了 lockA");
sleep(100);
synchronized (lockB) {
System.out.println("Thread-1 拿到了 lockB");
}
}
});
Thread t2 = new Thread(() -> {
synchronized (lockA) { // 注意:顺序统一
System.out.println("Thread-2 拿到了 lockA");
sleep(100);
synchronized (lockB) {
System.out.println("Thread-2 拿到了 lockB");
}
}
});
t1.start();
t2.start();
}
private static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException ignored) {}
}
}
3. 为什么这种方式最靠谱?
- 不依赖 JVM 特性
- 不依赖并发工具类
- 不增加复杂度
- 靠 设计约束 消灭死锁
在实际项目中,很多团队会直接把锁顺序写进编码规范。
解决方案二:使用 tryLock,避免无限等待
如果你用的是 ReentrantLock,那就有了更灵活的手段。
1. 核心思想
拿不到锁,就不要一直等。
2. 示例代码(可运行)
java
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.ReentrantLock;
public class TryLockDemo {
private static final ReentrantLock lockA = new ReentrantLock();
private static final ReentrantLock lockB = new ReentrantLock();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
try {
if (lockA.tryLock(1, TimeUnit.SECONDS)) {
System.out.println("Thread-1 拿到 lockA");
sleep(100);
if (lockB.tryLock(1, TimeUnit.SECONDS)) {
try {
System.out.println("Thread-1 拿到 lockB");
} finally {
lockB.unlock();
}
}
}
} catch (InterruptedException ignored) {
} finally {
if (lockA.isHeldByCurrentThread()) {
lockA.unlock();
}
}
});
Thread t2 = new Thread(() -> {
try {
if (lockB.tryLock(1, TimeUnit.SECONDS)) {
System.out.println("Thread-2 拿到 lockB");
sleep(100);
if (lockA.tryLock(1, TimeUnit.SECONDS)) {
try {
System.out.println("Thread-2 拿到 lockA");
} finally {
lockA.unlock();
}
}
}
} catch (InterruptedException ignored) {
} finally {
if (lockB.isHeldByCurrentThread()) {
lockB.unlock();
}
}
});
t1.start();
t2.start();
}
private static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException ignored) {}
}
}
3. tryLock 的真实价值
- 不会「卡死」
- 失败可重试
- 可上报监控
- 适合 非核心路径
但要注意:
- 代码复杂度会上升
- 释放锁必须非常小心
- 更适合中高级并发场景
解决方案三:使用并发包,减少"自己加锁"
很多死锁,本质原因只有一个:
你加了太多不必要的锁。
1. 示例:用 ConcurrentHashMap 替代 synchronized Map
java
import java.util.concurrent.ConcurrentHashMap;
public class ConcurrentDemo {
private static final ConcurrentHashMap<String, Integer> map =
new ConcurrentHashMap<>();
public static void main(String[] args) {
Thread t1 = new Thread(() -> {
map.put("A", 1);
map.computeIfAbsent("B", k -> 2);
});
Thread t2 = new Thread(() -> {
map.put("B", 3);
map.computeIfAbsent("A", k -> 4);
});
t1.start();
t2.start();
}
}
2. 为什么并发包更安全?
- 内部锁粒度更细
- 避免全局大锁
- 经过大量生产验证
在业务代码中:
- 能用并发容器,就别手写锁
- 能用原子类,就别 synchronized
一些非常实用的经验总结
结合实际项目,关于死锁我一般只记这几条:
- 死锁不是"偶发 bug",而是设计缺陷
- 多锁场景,第一反应永远是「顺序是否一致」
- tryLock 是兜底方案,不是万能解
- 并发包不是为了性能,而是为了安全
- 看到 synchronized 嵌套,警惕性要拉满
总结
死锁最可怕的地方不在于它难,而在于:
- 它不会立刻暴露
- 它只在高并发下出现
- 它往往发生在你最不想出问题的时候