死锁、活锁、饥饿&定位死锁&解决死锁

1. 死锁

死锁:一组互相竞争资源的线程因互相等待,导致“永久”阻塞的现象。

说白了就是:两个线程互相持有对方所需的资源,互不释放且互相等待

  • t1 线程获得A对象锁,接下来想获取B对象的锁
  • t2 线程获得B对象锁,接下来想获取A对象的锁

我们举个栗子:

public class DeadLock {

    public static void main(String[] args) {
        Object A = new Object(); //锁A
        Object B = new Object(); //锁B
        new Thread(()->{
            synchronized (A){
                log.debug("t1 lock A");
                sleep(0.5);
                synchronized (B){
                    log.debug("t1 lock B");
                    log.debug("操作...");
                }
            }
        },"t1").start();


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

    static void sleep(double time){
        try {
            Thread.sleep((int)(1000 * time));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

可以看到t1和t2各持有一把锁,当尝试获取对方线程锁的时候自己却不释放锁,这就导致死锁发生

2. 定位死锁

如果发生了死锁,那我们如何定位死锁呢?我们需要借助一些工具:

  • 基于命令行: jps 定位进程 id,再用 jstack 定位死锁

  • 基于图形化界面:可以使用 jconsole工具

2.1 jstack工具使用

我们首先在项目路径下使用JPS命令查看正在运行的Java线程

image-20220506203716053

接着我们可以使用jstack+可能发生死锁的线程ID打印线程情况:

image-20220506204054847

然后我们就可以看到一些线程的信息

image-20220506204647558

当然我们也能在最后面看到发生死锁的信息

image-20220506204920057

2.2 jconsole工具使用:

Jconsole (Java Monitoring and Management Console),是一种基于JMX的可视化监视、管理工具、

Jconsole 基本使用:

  • 点击JDK/bin 目录下面的jconsole.exe 即可启动
  • 然后会自动自动搜索本机运行的所有虚拟机进程。
  • 选择其中一个进程可开始进行监视

image-20220506205337535

可以看到本机所有的虚拟机进程

image-20220506205530750

检测死锁

image-20220506205848464

可以查看到线程发生死锁的信息和发生死锁的行数

image-20220506211021343

注意:

  • 避免死锁要注意加锁顺序
  • 另外如果由于某个线程进入了死循环,导致其它线程一直等待,对于这种情况 linux 下可以通过 top 先定位到 CPU 占用高的 Java 进程,再利用 top -Hp 进程id 来定位是哪个线程,最后再用 jstack 排查

3. 解决死锁

3.1 哲学家就餐问题

在解决死锁之前,我们先来通过一个经典的问题来分析一下死锁

image-20220506211454918

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

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

五位哲学家相当于五个线程,无根筷子相当于五份互斥的资源,当每个哲学家即线程持有一根筷子时,他们都在等待另一个线程释放锁,因此造成了死锁(典型的持有一个资源,等待对方释放资源的场景)

这种线程没有按预期结束,执行不下去的情况,归类为【活跃性】问题,除了死锁以外,还有活锁和饥饿者两种情况

下面通过具体的代码演示下这个死锁的问题:

筷子类:

public class Chopstick {
    String name;
    public Chopstick(String name) {
        this.name = name;
    }
    @Override
    public String toString() {
        return "筷子{" + name + '}';
    }
}

哲学家类:

public 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 TestPhilosopher {
    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();
    }

}

我们的程序运行一会后发生了死锁:

image-20220506213705769

使用Jconsole可以看到:

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

这种线程没有按预期结束,执行不下去的情况,归类为【活跃性】问题,除了死锁以外,还有 活锁饥饿者 两种情况

解决方案在第六节

4. 活锁

4.1 活锁原因

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

举个栗子:

public class LiveLock {
    static volatile int count = 10;
    static final Object lock = new Object();

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

观察程序运行结果,我们会发现这两个线程在一直运行,且不会停止,因为自己线程的停止条件被其他线程修改了,导致线程停止不了

活锁和死锁不同,死锁是线程发生堵塞,而活锁是线程不发生堵塞但是却不会停止

image-20220506215312901

4.2 活锁解决

  • 让互相影响的线程执行的顺序交错开
  • 睡眠的时间差异化
  • 在开发中可以通过增加随机睡眠时间来避免活锁

5. 饥饿

饥饿:线程因无法访问所需资源而无法执行下去的情况,说白了就是:假设有1万个线程,还没等前面的线程执行完,后面的线程就饿死了

很多教程中把饥饿定义为,一个线程由于优先级太低,始终得不到 CPU 调度执行,也不能够结束

先来看看使用顺序加锁的方式解决之前的死锁问题,就是两个线程对两个不同的对象加锁的时候都使用相同的顺序进行加锁

我们看下面的图,当线程1和线程2都分别持有一把锁(获取锁的顺序不一样),又想要获取对方锁的时候就会发生死锁情况

image-20220506215750664

我们可以通过顺序加锁的方式来避免这种死锁,即线程1和线程2都按A、B的顺序去获取锁

image-20220506223726810

显然这样就可以解决死锁的情况发生,但是这样又可能会导致饥饿的现象发生

这里以上面哲学家就餐的例子演示一下:

我们看之前的代码,都是按顺序获取锁

image-20220506225641716

我们现在将其改成不按顺序争抢锁

image-20220506225607689

我们观察运行结果就会发现一个神奇的现象

  • 首先并没有发生死锁
  • 赫拉克利特线程获取锁的频率最高,等运行几次后阿基米德线程就再也没有获取到锁了

image-20220506225924451

这样就是一种线程饥饿的现象发生了,即一些线程一直等不到cpu调度

6. 使用ReentrantLock解决死锁、活锁、饥饿

ReentrantLock相对于 synchronized 它具备如下特点

  1. 可中断
  2. 可以设置超时时间
  3. 可以设置为公平锁
  4. 支持多个条件变量,即对与不满足条件的线程可以放到不同的集合中等待

ReentrantLock基本使用思维导图

image-20220503003158484

6.1 解决哲学家就餐死锁问题

我们知道导致死锁必须的四个条件:

  1. 互斥条件:共享资源被一个线程占用
  2. 请求与保持条件(占有且等待):一个进程因请求资源而被阻塞时,对已经获得资源保持不释放
  3. 不可剥夺条件(不可抢占):进程已获得资源,在未使用完之前,不能进行剥夺
  4. 循环等待条件:多个线程 循环等待资源,而且是循环的互相等待

对应着如果想要避免死锁,我们只需要让上面的四个条件其中一个不成立即可

  1. 请求与保持条件:放大锁范围,去除对资源的抢占
  2. 不剥夺:换成可重入锁ReentrantLock
  3. 循环等待:改成顺序加锁,避免循环等待
  4. 互斥是多线程的特性,所以这个条件无法避免

我们可以看到发生死锁的代码主要是下面的代码

当前线程获得左手筷子线程时,由于右手筷子拿不到就会一直等待,导致死锁(锁不可剥夺)

// 获得左手筷子
synchronized (left) {
    // 获得右手筷子
    synchronized (right) {
        // 吃饭
        eat();
    }
    // 放下右手筷子
}
// 放下左手筷子

现在我们可以通过ReentrantLock的tryLock对上面的代码进行改进

首先让筷子类继承自ReentrantLock,让其拥有锁的性质

public class ReentrantLockChopstick extends ReentrantLock {...}

接着我们改写上面的代码

while (true) {
    // 获得左手筷子
    if(left.tryLock()){
        try {
            //尝试获得右手筷子
            if(right.tryLock()){
                try {
                	eat();
                } finally {
                    right.unlock();
                }
            }
        } finally {
            left.unlock();
        }
    }
    // 放下左手筷子
}

接着测试一下,我们会发现死锁问题解决

总之,死锁的问题就是要解决资源不可剥夺、循环等待、请求与保持条件等问题,而在Java并发包内提供了很多工具类和方法来为我们保证多线程下程序的安全性

  • 2
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
Java中的死锁活锁是多线程并发编程中的常见问题,这些问题可能会导致线程无法向前执行或陷入死循环。下面分别介绍Java中死锁活锁的概念和案例。 1. 死锁 死锁是指两个或多个线程相互等待对方释放锁,从而导致所有线程都无法向前执行的状态。简单来说,当两个或多个线程都占有某些资源并且又想要获取对方占有的资源时,就会发生死锁。 下面是一个Java中的死锁例子: ```java public class DeadlockExample { private Object lock1 = new Object(); private Object lock2 = new Object(); public void method1() { synchronized (lock1) { System.out.println("Method 1: Holding lock 1..."); try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lock2) { System.out.println("Method 1: Holding lock 1 and lock 2..."); } } } public void method2() { synchronized (lock2) { System.out.println("Method 2: Holding lock 2..."); try { Thread.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); } synchronized (lock1) { System.out.println("Method 2: Holding lock 1 and lock 2..."); } } } public static void main(String[] args) { DeadlockExample example = new DeadlockExample(); Thread t1 = new Thread(() -> example.method1()); Thread t2 = new Thread(() -> example.method2()); t1.start(); t2.start(); } } ``` 在这个例子中,两个线程t1和t2分别调用了method1和method2方法,这两个方法都需要获取lock1和lock2两个锁才能继续执行。由于t1占用了lock1并等待lock2,而t2占用了lock2并等待lock1,因此两个线程都无法释放自己占用的锁,从而陷入死锁状态。 2. 活锁 活锁是指线程们都在运行并尝试执行任务,但是由于某些条件始终无法满足,导致线程们一直在重试,但是最终无法完成任务。这种情况下,线程们看起来像是在不断地活动,但是实际上却没有任何进展。 下面是一个Java中的活锁例子: ```java public class LiveLockExample { private boolean isPolite; public LiveLockExample(boolean isPolite) { this.isPolite = isPolite; } public void bow(LiveLockExample partner) { while (true) { if (isPolite) { System.out.println("I'm polite, I'll bow first"); partner.bowBack(this); isPolite = false; break;
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值