【025】面试被问死锁?王二写的代码锁死服务器,哇哥 3 招根治!


Java中的死锁及其避免方法:

1、死锁定义: 多个线程因互相等待对方持有的锁而无法继续执行。

2、避免方法:

  • 避免一个线程同时获取多个锁。

  • 设置锁获取的超时时间。

  • 按顺序申请资源。


📙 作者: 编程技术圈(哇哥就业陪跑)
👉 欢迎关注、分享、评论
✔️ 持续分享更多干货内容
🌐🌏🌎➕tcmeta

零、引入

“服务器卡死了!CPU 100%,订单全卡着不动!” 技术部炸开锅,王二写的订单扣库存代码上线 10 分钟就把生产服务器干趴了,重启后没过 5 分钟又卡死。领导黑着脸把监控甩他脸上:“查不出问题,今天直接卷铺盖走!”

王二扒着代码看了半天,扣库存和扣余额的逻辑都加了锁,单测时顺风顺水,一到线上并发就僵死。隔壁哇哥端着泡满枸杞的保温杯过来,敲了敲键盘,用 jstack 命令调出一行红色日志:“你这是典型的死锁 —— 线程 A 拿着库存锁等余额锁,线程 B 拿着余额锁等库存锁,俩线程互相耗着,谁也不让谁,CPU 全空转了。今天把死锁搞懂,不仅能救你的工作,下次面试被问,你能把面试官讲懵。”

点赞 + 关注跟着哇哥和王二,用抢杯子和笔的例子搞懂死锁,3 招避免死锁的代码直接抄,还教你线上排查死锁的绝招!
在这里插入图片描述

一、先复现!王二的死锁代码,服务器卡死的元凶

王二的订单处理逻辑很简单:用户下单时,先扣商品库存,再扣用户余额,为了保证线程安全,他给库存和余额各加了一把锁。单线程测试时没问题,线上 100 个并发订单一来,直接卡死。

✅王二的 “锁死” 代码(必踩坑版)

请添加图片描述

package cn.tcmeta.deadlock;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

/**
 * @author: laoren
 * @description: 死锁
 * @version: 1.0.0
 */

// 订单处理系统——死锁版
public class DeadLockSample {
    // 库存锁:保护库存变量
    static final Object STOCK_LOCK = new Object();
    // 余额锁:保护余额变量
    static final Object BALANCE_LOCK = new Object();
    // 模拟库存和余额
    static int stock = 100;
    static int balance = 10000;
    // 100个并发订单任务
    static final int TASK_COUNT = 100;
    static CountDownLatch latch = new CountDownLatch(TASK_COUNT);

    public static void main(String[] args) throws InterruptedException {
        ExecutorService pool = Executors.newFixedThreadPool(20);

        for (int i = 0; i < TASK_COUNT; i++) {
            // 一半任务:先锁库存 → 再锁余额
            if (i % 2 == 0) {
                pool.submit(() -> {
                    try {
                        synchronized (STOCK_LOCK) {
                            System.out.println(Thread.currentThread().getName() + "拿到库存锁,等余额锁");
                            // 模拟业务耗时,放大死锁概率
                            Thread.sleep(100);
                            synchronized (BALANCE_LOCK) {
                                stock--;
                                balance -= 100;
                                System.out.println(Thread.currentThread().getName() + "扣减完成:库存=" + stock + ",余额=" + balance);
                            }
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        latch.countDown();
                    }
                });
            } else {
                // 另一半任务:先锁余额 → 再锁库存(锁顺序反了,必死锁)
                pool.submit(() -> {
                    try {
                        synchronized (BALANCE_LOCK) {
                            System.out.println(Thread.currentThread().getName() + "拿到余额锁,等库存锁");
                            Thread.sleep(100);
                            synchronized (STOCK_LOCK) {
                                stock--;
                                balance -= 100;
                                System.out.println(Thread.currentThread().getName() + "扣减完成:库存=" + stock + ",余额=" + balance);
                            }
                        }
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    } finally {
                        latch.countDown();
                    }
                });
            }
        }

        latch.await();
        pool.shutdown();
        System.out.println("所有订单处理完成"); // 永远执行不到,死锁了!
    }
}

在这里插入图片描述
运行结果:服务器卡死,CPU 飙到 100%
跑这段代码,控制台会停在 “拿到 XX 锁,等 XX 锁” 的日志,程序再也不往下走,CPU 直接拉满 —— 这就是死锁:多个线程拿着对方需要的锁,互相等待,谁也不放手,形成 “死循环”。

二、用 “抢杯子和笔”,讲透死锁的 4 个必要条件

“死锁其实特简单,就像你和同事抢东西,” 哇哥拿王二的水杯和自己的笔举例子,“你拿着杯子,想要我的笔;我拿着笔,想要你的杯子俩人为了等对方的东西,都不放手,就僵住了。死锁必须满足 4 个条件,少一个都锁不死:”

在这里插入图片描述

“只要打破其中一个条件,死锁就没了 —— 这是避免死锁的核心思路,比背 100 遍定义管用。”

三、3 招避免死锁,王二直接抄作业

在这里插入图片描述

🚀 第一招:固定锁的获取顺序(打破 “循环等待”)

最常用、最简单的方法:所有线程都按同一个顺序拿锁,比如 “先拿库存锁,再拿余额锁”,就算并发再高,也不会出现 “你等我、我等你” 的闭环。
修复代码(固定锁顺序)

package cn.tcmeta.deadlock;

import java.util.concurrent.*;

/**
 * @author: laoren
 * @description: 死锁修复版本
 * @version: 1.0.0
 */

// 订单处理系统——已修复死锁
public class DeadLockSample {

    // 库存锁:保护库存变量
    static final Object STOCK_LOCK = new Object();
    // 余额锁:保护余额变量
    static final Object BALANCE_LOCK = new Object();
    // 模拟库存和余额
    static int stock = 100;
    static int balance = 10000;
    // 并发订单任务数
    static final int TASK_COUNT = 100;
    static CountDownLatch latch = new CountDownLatch(TASK_COUNT);

    public static void main(String[] args) throws InterruptedException {
        // 自定义线程池配置,避免使用默认Executors工厂类带来的隐患
        ExecutorService pool = new ThreadPoolExecutor(
                20,
                20,
                0L,
                TimeUnit.MILLISECONDS,
                new LinkedBlockingQueue<>(TASK_COUNT),
                Executors.defaultThreadFactory(),
                new ThreadPoolExecutor.CallerRunsPolicy()
        );

        for (int i = 0; i < TASK_COUNT; i++) {
            pool.submit(() -> {
                try {
                    processOrder(); // 统一入口处理订单逻辑
                } finally {
                    latch.countDown();
                }
            });
        }

        latch.await();
        pool.shutdown();
        System.out.println("所有订单处理完成");
    }

    /**
     * 处理单笔订单的核心逻辑
     */
    private static void processOrder() {
        try {
            // 固定加锁顺序:始终先获取 STOCK_LOCK,再获取 BALANCE_LOCK,杜绝死锁
            synchronized (STOCK_LOCK) {
                System.out.println(Thread.currentThread().getName() + "拿到库存锁");
                synchronized (BALANCE_LOCK) {
                    stock--;
                    balance -= 100;
                    System.out.println(Thread.currentThread().getName() + "扣减完成:库存=" + stock + ",余额=" + balance);
                }
            }
        } catch (Exception e) {
            Thread.currentThread().interrupt(); // 恢复中断标志位
            e.printStackTrace();
        }
    }
}

在这里插入图片描述
哇哥划重点

“不管是订单、转账还是库存扣减,给锁定个‘优先级’(比如按锁对象的 hashCode 排序),所有线程都按这个顺序拿,循环等待的条件就没了,死锁根本发生不了。这招我用了 20 年,从没翻过车。”

📢 第二招:尝试加锁,超时放弃(打破 “持有并等待”)

用Lock的tryLock方法替代synchronized,拿锁超时就放弃,还释放已经拿到的锁 —— 不会 “拿着一个锁死等另一个锁”。

修复代码(tryLock 超时放弃)

package cn.tcmeta.deadlock;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author: laoren
 * @description: // 修复方案2:尝试加锁+超时放弃
 * @version: 1.0.0
 */
public class DeadLockFixByTryLock {
    static Lock stockLock = new ReentrantLock();
    static Lock balanceLock = new ReentrantLock();
    static int stock = 100;
    static int balance = 10000;
    static final int TASK_COUNT = 100;
    static CountDownLatch latch = new CountDownLatch(TASK_COUNT);

    public static void main(String[] args) throws InterruptedException {
        ExecutorService pool = Executors.newFixedThreadPool(20);

        for (int i = 0; i < TASK_COUNT; i++) {
            pool.submit(() -> {
                boolean stockLocked = false;
                boolean balanceLocked = false;
                try {
                    // 尝试拿库存锁,3秒超时
                    stockLocked = stockLock.tryLock(3, TimeUnit.SECONDS);
                    if (!stockLocked) {
                        System.out.println(Thread.currentThread().getName() + "拿库存锁超时,放弃执行");
                        return;
                    }
                    System.out.println(Thread.currentThread().getName() + "拿到库存锁,尝试拿余额锁");

                    // 尝试拿余额锁,3秒超时
                    balanceLocked = balanceLock.tryLock(3, TimeUnit.SECONDS);
                    if (!balanceLocked) {
                        System.out.println(Thread.currentThread().getName() + "拿余额锁超时,释放库存锁");
                        stockLock.unlock(); // 释放已拿到的锁,避免占着茅坑不拉屎
                        return;
                    }

                    // 扣减逻辑
                    stock--;
                    balance -= 100;
                    System.out.println(Thread.currentThread().getName() + "扣减完成:库存=" + stock + ",余额=" + balance);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                } finally {
                    // 释放所有拿到的锁
                    if (balanceLocked) balanceLock.unlock();
                    if (stockLocked) stockLock.unlock();
                    latch.countDown();
                }
            });
        }

        latch.await();
        pool.shutdown();
        System.out.println("所有订单处理完成!最终库存=" + stock + ",余额=" + balance);
    }
}

在这里插入图片描述
王二拍腿叫好
“这招太适合支付场景了!就算拿锁失败,也能返回‘系统繁忙,请重试’,不会让用户一直等,体验比卡死强 10 倍!”

哇哥提醒

“记住:tryLock拿到锁后,一定要在finally里释放,不然就算没死锁,也会出现‘锁泄露’,慢慢把服务器堵死。”

✔️ 第三招:一次性获取所有锁(打破 “持有并等待”)

在这里插入图片描述

要么把需要的锁全拿到手,要么一个都不拿 —— 彻底避免 “拿着 A 锁等 B 锁” 的情况。

修复代码(一次性拿所有锁)

package cn.tcmeta.deadlock;

import java.util.concurrent.CountDownLatch;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;

/**
 * @author: laoren
 * @description: // 修复方案3:一次性获取所有锁
 * @version: 1.0.0
 */
public class DeadLockFixByAllLock {

    static Lock stockLock = new ReentrantLock();
    static Lock balanceLock = new ReentrantLock();
    static int stock = 100;
    static int balance = 10000;
    static final int TASK_COUNT = 100;
    static CountDownLatch latch = new CountDownLatch(TASK_COUNT);

    // 工具方法:一次性拿所有锁,拿不到就释放已拿的
    private static boolean tryAcquireAllLocks(Lock... locks) throws InterruptedException {
        for (Lock lock : locks) {
            if (!lock.tryLock(3, TimeUnit.SECONDS)) {
                for (Lock l : locks) {
                    if (l instanceof ReentrantLock && ((ReentrantLock) l).isHeldByCurrentThread()) {
                        l.unlock();
                    }
                }
                return false;
            }
        }
        return true;
    }

    public static void main(String[] args) throws InterruptedException {
        ExecutorService pool = Executors.newFixedThreadPool(20);

        for (int i = 0; i < TASK_COUNT; i++) {
            pool.submit(() -> {
                boolean allLocked = false;
                try {
                    // 一次性拿库存锁+余额锁
                    allLocked = tryAcquireAllLocks(stockLock, balanceLock);
                    if (!allLocked) {
                        System.out.println(Thread.currentThread().getName() + "拿不到所有锁,放弃执行");
                        return;
                    }

                    // 扣减逻辑
                    stock--;
                    balance -= 100;
                    System.out.println(Thread.currentThread().getName() + "扣减完成:库存=" + stock + ",余额=" + balance);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                } finally {
                    // 释放所有锁
                    if (allLocked) {
                        balanceLock.unlock();
                        stockLock.unlock();
                    }
                    latch.countDown();
                }
            });
        }

        latch.await();
        pool.shutdown();
        System.out.println("所有订单处理完成!最终库存=" + stock + ",余额=" + balance);
    }
}

在这里插入图片描述

适用场景
“适合需要多把锁的复杂业务,比如转账(扣 A 账户 + 加 B 账户)、跨表操作,一次性拿锁能从根源上避免持有并等待。”

四、线上死锁了?用 jstack 快速排查

“就算不小心写出死锁,也不用慌,Java 自带的 jstack 命令能 5 秒定位问题,” 哇哥教王二实操:
排查步骤(傻瓜式)

  • 找进程 ID:打开终端,输入jps,找到死锁程序的 PID(比如1234 DeadLockDemo);
  • 打印线程日志:输入jstack 1234,回车;
  • 找死锁日志:日志里会标红Deadlock,还会告诉你哪个线程拿着哪个锁、等哪个锁,一目了然。

示例死锁日志(关键片段)

Found one Java-level deadlock:
=============================
Thread-1:
  waiting to lock monitor 0x00007f9e12006800 (object 0x000000076ab82060, a java.lang.Object),
  which is held by Thread-2
Thread-2:
  waiting to lock monitor 0x00007f9e12008000 (object 0x000000076ab82070, a java.lang.Object),
  which is held by Thread-1

“看,Thread-1 拿着余额锁等库存锁,Thread-2 拿着库存锁等余额锁,死锁实锤了!改下锁顺序就能解决。”

五、总结:死锁的 “防坑心法”

哇哥把核心要点缩成 3 句顺口溜,王二抄在便利贴贴显示器上:

  • 死锁四条件,破一就完蛋:互斥、持有等待、不可剥夺、循环等待,打破任一就能防死锁;
  • 锁序要固定,优先用这招:最简单、最稳的避坑方法,新手直接无脑用;
  • 超时加放弃,复杂场景上:核心业务用 tryLock,避免卡死服务器;
  • 排查用 jstack,线上不慌张:死锁了别重启,先 jstack 定位,再改代码。

哇哥的血泪彩蛋

“我刚工作时,写转账代码没固定锁顺序,把公司测试库搞死锁了,” 哇哥捂脸,“DBA 查了半天,最后重启数据库才恢复,我被罚写了 5000 字检讨 —— 从那以后,我写多锁代码必先定顺序,刻进 DNA 了!”

六、最后说句实在的

在这里插入图片描述

死锁看着吓人,其实就是 “线程互相卡脖子”,只要掌握 “打破必要条件” 的思路,就能轻松避免。今天这 3 套修复代码,你复制过去改改变量名就能用,不管是订单处理、转账系统还是库存扣减,都能避开死锁坑。

如果这篇救了你的服务器,点赞 + 分享给你那还在瞎写锁的同事!关注我,下次扒一扒 “活锁和饥饿”—— 比死锁更隐蔽的并发坑,让你面试时直接碾压面试官!

在这里插入图片描述
在这里插入图片描述
请添加图片描述

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值