用哲学家就餐问题解释死锁

89 阅读3分钟

《用哲学家就餐问题解释死锁》


场景还原

        哲学家D
       🥢5    🥢1
    哲学家C      哲学家A
       🥢4    🥢2
        哲学家B
           🥢3

剧情设定
5个秃头程序员(哲学家)转行做厨师,必须同时拿到左右两边的筷子(服务器资源)才能干饭(处理请求)。但筷子数量 == 哲学家数量,引发高并发修罗场。


死锁四要素的爆笑解读

  1. 互斥:筷子是独占资源 -> "我的筷子你别碰,就像生产环境的Redis锁"
  2. 占有且等待:拿着左筷等右筷 -> "就像你占着测试环境不释放还抢预发环境"
  3. 不可抢占:不能抢别人手里的筷子 -> "总不能拔掉同事的网线抢数据库连接吧"
  4. 循环等待:A等B的筷子,B等C的... -> "像极了跨团队甩锅时的责任闭环"

死亡代码演示(完整可运行)

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

public class DiningPhilosophers {

    // 五根筷子(锁对象)
    static Lock[] chopsticks = {
        new ReentrantLock(),
        new ReentrantLock(),
        new ReentrantLock(),
        new ReentrantLock(),
        new ReentrantLock()
    };

    public static void main(String[] args) {
        // 创建五个哲学家线程
        for (int i = 0; i < 5; i++) {
            final int philosopher = i;
            new Thread(() -> {
                while (true) {
                    // 先拿左边筷子(对5号哲学家来说左边是0号筷子)
                    chopsticks[philosopher].lock();
                    System.out.println("哲学家" + philosopher + "拿起左筷" + philosopher);
                    
                    // 再拿右边筷子(取模处理环形结构)
                    int right = (philosopher + 1) % 5;
                    chopsticks[right].lock();
                    System.out.println("哲学家" + philosopher + "拿起右筷" + right);

                    // 开始干饭(用随机时间模拟业务处理)
                    System.out.println("哲学家" + philosopher + "暴风吸入中...");
                    try { Thread.sleep((long)(Math.random()*1000)); } 
                    catch (InterruptedException e) {}

                    // 放下筷子
                    chopsticks[right].unlock();
                    chopsticks[philosopher].unlock();
                }
            }).start();
        }
    }
}

运行结果

哲学家0拿起左筷0
哲学家1拿起左筷1
哲学家2拿起左筷2
哲学家3拿起左筷3
哲学家4拿起左筷4
(程序卡死,无人能拿到两根筷子)

破局之道:阿里P7的祖传解决方案

方案一:资源排序法(破坏循环等待)

// 修改拿筷子的顺序
if (philosopher % 2 == 0) {
    // 偶数编号哲学家先拿左后拿右
    pickLeftThenRight();
} else {
    // 奇数编号哲学家先拿右后拿左
    pickRightThenLeft();
}

方案二:超时放弃(避免无限等待)

if (chopsticks[philosopher].tryLock(500, TimeUnit.MILLISECONDS)) {
    if (chopsticks[right].tryLock(500, TimeUnit.MILLISECONDS)) {
        // 成功拿到两把筷子
    } else {
        // 释放已获得的筷子
        chopsticks[philosopher].unlock(); 
    }
}

方案三:服务员协调(类似数据库死锁检测)

// 使用一个全局锁作为协调者
Lock butler = new ReentrantLock();

void eat() {
    butler.lock();
    try {
        pickLeftThenRight();
    } finally {
        butler.unlock();
    }
}

面试暴击三连

  1. 基础题:画图说明死锁四要素如何体现在该场景
    (答案示例:用筷子图标注互斥资源与等待环)

  2. 进阶题:如果使用Synchronized关键字会怎样?
    (危险操作:synchronized无法中断,死锁更难检测)

  3. 骚操作题:如何用jstack检测这段代码的死锁?
    (实战演示:运行后使用jstack <pid>查看死锁线程信息)


技术冷笑话精选

  • "这段代码就像产品经理的需求——看起来都能跑,实际上谁也动不了"
  • "当哲学家开始互相等待,就像你在等前端联调,前端在等测试环境"
  • "解决死锁的最好办法:把哲学家变成素食主义者(减少资源竞争)"

下期预告:《IOC容器の千层套路》