死锁

129 阅读3分钟

一个线程同时需要获取多把锁,这时就容易造成死锁
t1线程获得A对象锁,接下来想获取B对象的锁
t2线程获得B对象锁,接下来想获取A对象的锁

死锁案例

    Object A = new Object();
    Object B = new Object();
    
    Thread t1 = new Thread(() -> {
        synchronized (A) {
            log.debug("lock A");
            sleep(1);
            synchronized (B) {
                log.debug("lock B");
                log.debug("操作...");
            }
        }
    }, "t1");
    
    Thread t2 = new Thread(() -> {
        synchronized (B) {
            log.debug("lock B");
            sleep(0.5);
            synchronized (A) {
                log.debug("lock A");
                log.debug("操作...");
            }
        }
    }, "t2");
t1.start();
t2.start()

哲学家就餐

image.png 有五位哲学家,围坐在圆桌旁。

  • 他们只做两件事,思考和吃饭,思考一会吃口饭,吃完饭后接着思考。
  • 吃饭时要用两根筷子吃,桌上共有 5 根筷子,每位哲学家左右手边各有一根筷子。
  • 如果筷子被身边的人拿着,自己就得等待。

代码演示:

筷子类:

class Chopstick {

    String name;
    
    public Chopstick(String name) {
        this.name = name;
    }

    @Override
    public String toString() {
        return "筷子{" + name + '}';
    }
}

哲学家类

class Philosopher extends Thread {
    Chopstick left;
    Chopstick right;

    public Philosopher(String name, Chopstick left, Chopstick right) {
        super(name);
        this.left = left;
        this.right = right;
    }

    private void eat() {
        log.debug("eating...");
        Sleeper.sleep(1);
    }

    @Override
    public void run() {
        while (true) {
            // 获得左手筷子
            synchronized (left) {
                // 获得右手筷子
                synchronized (right) {
                    // 吃饭
                    eat();
                }
                // 放下右手筷子
            }
            // 放下左手筷子
        }
    }
}

就餐

public class Test {
    public static void main(String[] args) {
    
        Chopstick c1 = new Chopstick("1");
        Chopstick c2 = new Chopstick("2");
        Chopstick c3 = new Chopstick("3");
        Chopstick c4 = new Chopstick("4");
        Chopstick c5 = new Chopstick("5");
        new Philosopher("苏格拉底", c1, c2).start();
        new Philosopher("柏拉图", c2, c3).start();
        new Philosopher("亚里士多德", c3, c4).start();
        new Philosopher("赫拉克利特", c4, c5).start();
        new Philosopher("阿基米德", c5, c1).start();
    }
}

执行结果:

12:33:15.575 [苏格拉底] c.Philosopher - eating...
12:33:15.575 [亚里士多德] c.Philosopher - eating...
12:33:16.580 [阿基米德] c.Philosopher - eating...
12:33:17.580 [阿基米德] c.Philosopher - eating...
// 卡在这里, 不向下运行

使用 jconsole 检测死锁,发现

-------------------------------------------------------------------------
名称: 阿基米德
状态: cn.itcast.Chopstick\@1540e19d (筷子1) 上的BLOCKED
拥有者: 苏格拉底
总阻止数: 2, 总等待数: 1
堆栈跟踪:cn.itcast.Philosopher.run(TestDinner.java:48)
已锁定 cn.itcast.Chopstick\@6d6f6e28 (筷子5)
-------------------------------------------------------------------------
名称: 苏格拉底
状态: cn.itcast.Chopstick\@677327b6 (筷子2) 上的BLOCKED
拥有者: 柏拉图
总阻止数: 2, 总等待数: 1
堆栈跟踪:cn.itcast.Philosopher.run(TestDinner.java:48)
已锁定 cn.itcast.Chopstick\@1540e19d (筷子1)
-------------------------------------------------------------------------
名称: 柏拉图
状态: cn.itcast.Chopstick\@14ae5a5 (筷子3) 上的BLOCKED
拥有者: 亚里士多德
总阻止数: 2, 总等待数: 0
堆栈跟踪:cn.itcast.Philosopher.run(TestDinner.java:48)
已锁定 cn.itcast.Chopstick\@677327b6 (筷子2)
-------------------------------------------------------------------------
名称: 亚里士多德
状态: cn.itcast.Chopstick\@7f31245a (筷子4) 上的BLOCKED
拥有者: 赫拉克利特
总阻止数: 1, 总等待数: 1
堆栈跟踪:cn.itcast.Philosopher.run(TestDinner.java:48)
已锁定 cn.itcast.Chopstick\@14ae5a5 (筷子3)
-------------------------------------------------------------------------
名称: 赫拉克利特
状态: cn.itcast.Chopstick\@6d6f6e28 (筷子5) 上的BLOCKED
拥有者: 阿基米德
总阻止数: 2, 总等待数: 0
堆栈跟踪:cn.itcast.Philosopher.run(TestDinner.java:48)
已锁定 cn.itcast.Chopstick\@7f31245a (筷子4)
-------------------------------------------------------------------------

活锁

活锁出现在两个线程互相改变对方的结束条件,最后谁也无法结束,例如

public class TestLiveLock {

    static volatile int count = 10;
    
    static final Object lock = new Object();
    
    public static void main(String[] args) {
        new Thread(() -> {
            // 期望减到 0 退出循环
            while (count > 0) {
                sleep(0.2);
                count--;
                log.debug("count: {}", count);
            }
        }, "t1").start();
        
        new Thread(() -> {
            // 期望超过 20 退出循环
            while (count < 20) {
                sleep(0.2);
                count++;
                log.debug("count: {}", count);
            }
        }, "t2").start();
    }
}

饥饿

一个线程由于优先级太低,始终得不到CPU调度执行,也不能够结束,读写锁时会涉及饥饿问题。以下有一个线程饥饿的例子,先来看看使用顺序加锁的方式解决之前的死锁问题. image.png 顺序加锁的解决方案 image.png

定位死锁

检测死锁可以使用jconsole工具(推荐),或者使用jps定位进程id,再用jstack定位死锁。 在cmd界面输入jconsole就可以打开界面,点击线程即可查看。 image.png