Java死锁这只“吞金兽”,是不是也曾让你在凌晨三点的生产环境里怀疑人生?

举报
喵手 发表于 2025/12/08 20:35:59 2025/12/08
【摘要】 开篇语哈喽,各位小伙伴们,你们好呀,我是喵手。运营社区:C站/掘金/腾讯云/阿里云/华为云/51CTO;欢迎大家常来逛逛  今天我要给大家分享一些自己日常学习到的一些知识点,并以文字的形式跟大家一起交流,互相学习,一个人虽可以走的更快,但一群人可以走的更远。  我是一名后端开发爱好者,工作日常接触到最多的就是Java语言啦,所以我都尽量抽业余时间把自己所学到所会的,通过文章的形式进行输出,...

开篇语

哈喽,各位小伙伴们,你们好呀,我是喵手。运营社区:C站/掘金/腾讯云/阿里云/华为云/51CTO;欢迎大家常来逛逛

  今天我要给大家分享一些自己日常学习到的一些知识点,并以文字的形式跟大家一起交流,互相学习,一个人虽可以走的更快,但一群人可以走的更远。

  我是一名后端开发爱好者,工作日常接触到最多的就是Java语言啦,所以我都尽量抽业余时间把自己所学到所会的,通过文章的形式进行输出,希望以这种方式帮助到更多的初学者或者想入门的小伙伴们,同时也能对自己的技术进行沉淀,加以复盘,查缺补漏。

小伙伴们在批阅的过程中,如果觉得文章不错,欢迎点赞、收藏、关注哦。三连即是对作者我写作道路上最好的鼓励与支持!

前言:一个悲伤的故事

咱们先不谈技术,先谈谈感情。💔

想象一下,这是一个风和日丽的周五下午,你刚刚提交了最后一行代码,甚至已经在那心里盘算着晚上是去整顿小烧烤还是回家躺平打游戏。突然,运维大哥那个只有出事才会响的钉钉群疯狂@你:“兄弟!生产环境不动了!CPU占用不高,内存也没爆,但请求就是进不去,日志也不打印了!”

那一刻,你的心跳是不是漏了一拍?这种“既不报错也不干活”的僵尸状态,十有八九就是**死锁(Deadlock)**这位大爷光临了。说实话,我自己当年也是在无数个这种绝望的夜晚,盯着屏幕上的 jstack 输出,恨不得钻进显示器里把那两条打架的线程给掰开。今天,咱们就扒开Java并发这层神秘的面纱,看看这死锁到底是个什么妖魔鬼怪,以及咱们手里有哪些“降妖除魔”的法宝。

一、 到底啥是死锁?(说点人话版)

别跟我扯什么“哲学家就餐问题”,那玩意儿太学术了,听着就想睡。😴

咱们用个大白话来解释:
  假设你有两把钥匙,A和B。
  线程老王手里拿着钥匙A,但他非得要钥匙B才能把活儿干完;
  同时,线程老张手里拿着钥匙B,但他死活要钥匙A才能交差。
  这俩人谁也不肯先撒手,就这么干瞪眼,僵持到了天荒地老。这就是死锁。简单吧?但在代码里,这事儿发生得那是相当隐蔽,简直就是“润物细无声”地把你的系统搞挂。

二、 案发现场还原:一段能把电脑跑傻的代码

光说不练假把式。来,咱们写一段“有毒”的代码,亲手制造一个案发现场。各位看官,千万别在生产环境跑这段代码,否则被祭天了可别怪我没提醒啊!⚠️

这是一个经典的嵌套锁(Nested Lock)导致的死锁案例:

/**
 * DeadlockDemo.java
 * 一个极其做作的死锁演示,专门用来坑初学者
 */
public class DeadlockDemo {

    // 创建两把锁,这俩就是祸水的根源
    private static final Object lockA = new Object();
    private static final Object lockB = new Object();

    public static void main(String[] args) {
        
        // 线程1:我是头铁娃,我先拿A,再去要去B
        Thread thread1 = new Thread(() -> {
            synchronized (lockA) {
                System.out.println("🧵 线程1:嘿嘿,我拿到锁A了!正在试图去拿锁B...");
                try {
                    // 稍微睡会儿,为了让线程2有机会拿到锁B,这一步很关键!
                    // 就像你抢到了遥控器,非要先去上个厕所
                    Thread.sleep(100); 
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                
                synchronized (lockB) {
                    System.out.println("🧵 线程1:这一行你永远看不见,因为我卡死了!");
                }
            }
        }, "Thread-No-1");

        // 线程2:我也是头铁娃,我先拿B,再去要去A
        Thread thread2 = new Thread(() -> {
            synchronized (lockB) {
                System.out.println("🧵 线程2:哈哈,我拿到锁B了!正在试图去拿锁A...");
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                
                synchronized (lockA) {
                    System.out.println("🧵 线程2:如果能看到我,说明死锁没发生(做梦呢)");
                }
            }
        }, "Thread-No-2");

        thread1.start();
        thread2.start();
        
        System.out.println("🚦 主线程:由于那俩傻子互相卡住了,程序永远不会结束...");
    }
}

运行结果:
  你会看到控制台打印了两句豪言壮语后,就彻底陷入了死一般的沉寂。你的程序就像被点了穴一样,除了强制结束(Kill),没有任何办法唤醒它。这就是最典型的资源互斥且循环等待

三、 怎么破案?(排查死锁的侦探三件套)

好了,假如这事儿真在生产环境发生了(呸呸呸,乌鸦嘴),咱们怎么确认是不是死锁呢?总不能靠猜吧?这时候就得掏出咱们JDK自带的“法医工具箱”了。🕵️‍♂️

1. 简单的 jps + jstack (命令行才是男人的浪漫)

首先,你得知道那个死掉的Java进程是谁。
  在终端敲入:
  jps -l
  假设你看到了一个 12345 com.example.DeadlockDemo,这个 12345 就是PID。

接下来,重头戏来了,使用 jstack 抓取线程快照:
  jstack 12345

输出的内容会很长,甚至有点吓人,但你别慌!直接拉到最下面,JDK非常贴心地(虽然平时不怎么贴心)给你总结了一段话:

Found one Java-level deadlock:
=============================
"Thread-No-1":
  waiting to lock monitor 0x000000001f3... (object 0x000000076b..., a java.lang.Object),
  which is held by "Thread-No-2"
"Thread-No-2":
  waiting to lock monitor 0x000000001f4... (object 0x000000076a..., a java.lang.Object),
  which is held by "Thread-No-1"

Java stack information for the threads listed above:
===================================================
"Thread-No-1":
    at com.example.DeadlockDemo.lambda$main$0(DeadlockDemo.java:25)
    - waiting to lock <0x000000076b5d9d40> (a java.lang.Object)
    - locked <0x000000076b5d9d30> (a java.lang.Object)
...

看到 Found one Java-level deadlock 这几个字了吗?这简直就是“确诊书”啊!它明明白白地告诉你:Thread-1在等Thread-2,Thread-2在等Thread-1。案情水落石出!🎉

2. 图形化界面 VisualVMJConsole

如果你觉得黑底白字的命令行太伤眼睛,或者想给老板演示的时候显得“高大上”一点,可以用 jvisualvm。打开它,连上你的进程,点一下“Threads”标签页。如果检测到死锁,它会直接给你弹个大大的红色警告:“检测到死锁!”。那画面,那叫一个触目惊心又赏心悦目。

四、 怎么预防?(千万别等出事了再修)

排查是事后诸葛亮,预防才是王道。咱们怎么写代码才能避免这种尴尬的局面呢?这里我有三板斧,招招致命。🪓

策略一:定好规矩,长幼有序(顺序加锁)

绝大多数死锁都是因为加锁顺序不一致导致的。就像咱们上面那个例子,一个先拿A再拿B,一个先拿B再拿A,这不打架才怪。

解决办法: 强行规定,所有线程必须按照相同的顺序拿锁。比如,谁要想干活,必须先拿LockA,再拿LockB。谁要是敢先拿LockB,直接在Code Review阶段就把他腿打折(开玩笑的,打回重写就行)。

只要顺序一致,就不可能形成“环路”,死锁自然就不攻自破了。这招最简单,也最有效,就是稍微有点废脑子,得时刻记着顺序。

策略二:别傻等了,不行就撤(使用 Lock.tryLock

synchronized 关键字有个硬伤,就是它太执着了,一旦拿不到锁,就死等,等到海枯石烂也不回头。咱们可以用JDK 5引入的 ReentrantLock 来代替它。

ReentrantLock 有个神技叫 tryLock()。这玩意儿就像你去追女神,问一句:“能做我女朋友吗?”
  如果是 synchronized,女神不答应你就一直在那跪着,直到饿死。
  如果是 tryLock,女神不答应,你就可以说:“行,那我过会儿再来问,或者我先去打会儿球。” 🏀

代码演示:

import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.concurrent.TimeUnit;
import java.util.Random;

public class TryLockDemo {
    private static final Lock lockA = new ReentrantLock();
    private static final Lock lockB = new ReentrantLock();

    public static void main(String[] args) {
        new Thread(() -> doTask(lockA, lockB, "线程1"), "Thread-1").start();
        new Thread(() -> doTask(lockB, lockA, "线程2"), "Thread-2").start();
    }

    private static void doTask(Lock firstLock, Lock secondLock, String name) {
        try {
            while (true) {
                // 尝试拿第一把锁,不等,拿到就进,拿不到就拉倒
                if (firstLock.tryLock()) {
                    try {
                        System.out.println(name + " 拿到了第一把锁");
                        
                        // 尝试拿第二把锁,只等50毫秒
                        if (secondLock.tryLock(50, TimeUnit.MILLISECONDS)) {
                            try {
                                System.out.println(name + " 拿到了第二把锁,任务完成!🎉");
                                break; // 成功了,撤!
                            } finally {
                                secondLock.unlock();
                            }
                        } else {
                            System.out.println(name + " 拿第二把锁失败,为了避免死锁,我主动放弃第一把锁!💨");
                        }
                    } finally {
                        firstLock.unlock();
                    }
                }
                // 稍微休息下再重试,防止活锁(两人在走廊互相谦让谁也过不去)
                Thread.sleep(new Random().nextInt(100));
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

这段代码就灵活多了!一旦发现情况不对,立马释放手里的资源,“留得青山在,不怕没柴烧”。这种破坏“占有且等待”条件的方法,是解决死锁的另一大利器。

策略三:减小锁的粒度(别把整个世界都锁住)

有时候死锁是因为咱们太贪心了,为了图省事,直接在一个大方法上加 synchronized。这就像是为了防止有人偷你家马桶,结果把你家整个小区都封锁了一样。🔒

建议: 只锁那些真正需要保护的共享数据。锁的范围越小,冲突的概率就越低,死锁的可能性自然也就微乎其微了。能锁代码块,就别锁方法;能用原子类(AtomicInteger),就别用锁。

五、 写在最后:给兄弟们打个气

说实话,并发编程这块水真的很深。哪怕是写了十几年代码的老油条(比如我),偶尔也会在复杂的业务逻辑里翻车。遇到死锁别灰心,这说明你的系统业务量上来了,复杂度上来了,这是好事儿啊!📈

synchronized 的死磕到底,到 ReentrantLock 的灵活变通,再到无锁编程的极致性能,每一次对锁的思考,其实都是咱们段位的提升。

下次再遇到程序卡死,先别急着重启,淡定地敲一个 jstack,看着那一堆堆栈信息,嘴角微微上扬:“小样儿,我看你往哪儿跑!”这才是咱们技术人的排面!😎

好了,今天的砖就搬到这儿。如果你觉得这篇文章让你少掉了几根头发,记得点个赞或者留个言,咱们评论区接着唠!👋

… …

文末

好啦,以上就是我这期的全部内容,如果有任何疑问,欢迎下方留言哦,咱们下期见。

… …

学习不分先后,知识不分多少;事无巨细,当以虚心求教;三人行,必有我师焉!!!

wished for you successed !!!


⭐️若喜欢我,就请关注我叭。

⭐️若对您有用,就请点赞叭。
⭐️若有疑问,就请评论留言告诉我叭。


版权声明:本文由作者原创,转载请注明出处,谢谢支持!

【声明】本内容来自华为云开发者社区博主,不代表华为云及华为云开发者社区的观点和立场。转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息,否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。