Java程序死锁问题定位与解决方法

 更新时间:2024年11月27日 09:37:30   作者:徐俊生  
死锁是一种特定的程序状态,主要是由于循环依赖导致彼此一直处于等待中,而使得程序陷入僵局,相当尴尬,死锁不仅仅发生在线程之间,而对于资源独占的进程之间同样可能出现死锁,本文给大家介绍了Java程序死锁问题定位与解决方法,需要的朋友可以参考下

1. 死锁概述

1.1 什么是死锁

  1. 一定是发生在并发中;
  2. 互不相让:当两个(或更多)线程(或进程)相互持有对方所要的资源,又不主动释放,导致程序陷入无尽的阻塞,这就是死锁。

1.2 死锁产生的必要条件

导致死锁的条件有四个,这四个条件同时满足就会产生死锁。

  • 互斥条件:某些资源只能由一个线程独占使用,其他线程在资源被占用时只能等待。
  • 请求和保持条件:一个线程因请求资源而阻塞时,对已获得的资源保持不放。
  • 不可抢占条件:线程已获得的资源,在未使用完之前,不能强行剥夺。
  • 循环等待条件:若干线程之间形成一种头尾相接的循环等待资源关系。

2. 死锁的案例分析

public class Resource {
    private String name;
    private int count;

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

    public void staticResource() {
        synchronized (this) {
            System.out.println("static resource");
            count++;
        }
    }

    public void saveResource(Resource resource) {
        synchronized (this) {
            System.out.println("save resource:" + Thread.currentThread().getName());
            resource.staticResource();
        }
    }
}
public class DeadLock {
    public static void main(String[] args) {
        Resource resource1 = new Resource("resource1");
        Resource resource2 = new Resource("resource2");
        Thread threadA = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                resource1.saveResource(resource2);
            }
        });

        Thread threadB = new Thread(() -> {
            for (int i = 0; i < 100; i++) {
                resource2.saveResource(resource1);
            }
        });

        threadA.start();
        threadB.start();
    }
}

打印结果:

save resource:Thread-0
save resource:Thread-1

死锁原因分析:

  1. 线程 A 行为:
  • threadA 在调用 resource1.saveResource(resource2) 时:
    • 首先锁住了 resource1 对象。
    • 然后试图锁住 resource2 对象,进入其 staticResource 方法。
  1. 线程B行为:
  • threadB 在调用 resource2.saveResource(resource1) 时:
    • 首先锁住了 resource2 对象。
    • 然后试图锁住 resource1 对象,进入其 staticResource 方法。
  1. 死锁发生的原因:
  • 如果 threadA 已经锁住 resource1,并等待锁住 resource2,而此时 threadB 已经锁住 resource2 并等待锁住 resource1,就会发生循环等待。
  • 两个线程互相等待对方释放锁,从而陷入死锁状态。

3. 死锁排查

  1. 首先,通过 jps 命令,查看 Java 进程的 pid。
C:\Users\shawn>jps
22568
24488 Launcher
10060 DeadLock
28076 Jps
  • 然后,通过 jstack <pid> 命令查看线程 dump 日志。当发现死锁时,可以在打印的 dump 日志中找到Found one Java-level deadlock: 信息,根据信息的内容可以分析死锁出现的原因。
C:\Users\shawn>jstack 23128
2024-11-23 15:38:34
Full thread dump Java HotSpot(TM) 64-Bit Server VM (25.321-b07 mixed mode):
=============================
Found one Java-level deadlock:
=============================
"Thread-1":
  waiting to lock monitor 0x0000022b6c713f08 (object 0x000000076bdaa990, a com.atu.deadlock.Resource),
  which is held by "Thread-0"
"Thread-0":
  waiting to lock monitor 0x0000022b6c7169a8 (object 0x000000076bdaa9e8, a com.atu.deadlock.Resource),
  which is held by "Thread-1"

Java stack information for the threads listed above:
===================================================
"Thread-1":
        at com.atu.deadlock.Resource.staticResource(Resource.java:13)
        - waiting to lock <0x000000076bdaa990> (a com.atu.deadlock.Resource)
        at com.atu.deadlock.Resource.saveResource(Resource.java:21)
        - locked <0x000000076bdaa9e8> (a com.atu.deadlock.Resource)
        at com.atu.deadlock.DeadLock.lambda$main$1(DeadLock.java:18)
        at com.atu.deadlock.DeadLock$$Lambda$2/1096979270.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:750)
"Thread-0":
        at com.atu.deadlock.Resource.staticResource(Resource.java:13)
        - waiting to lock <0x000000076bdaa9e8> (a com.atu.deadlock.Resource)
        at com.atu.deadlock.Resource.saveResource(Resource.java:21)
        - locked <0x000000076bdaa990> (a com.atu.deadlock.Resource)
        at com.atu.deadlock.DeadLock.lambda$main$0(DeadLock.java:12)
        at com.atu.deadlock.DeadLock$$Lambda$1/1324119927.run(Unknown Source)
        at java.lang.Thread.run(Thread.java:750)

Found 1 deadlock.

4. 线上发生死锁应该怎么办

  1. 首先保存案发现场,然后立刻重启服务器(使用 java 相应的命令把整个堆栈信息保存下来),不能进一步影响用户体验;
  2. 暂时保证线上服务的安全,然后再利用刚才保存的信息,排查死锁,修改代码,重新发版。

5. 常见死锁修复策略

前面我们说死锁的四个必要条件,我们只需要破坏其中任意一个,就可以避免死锁的产生。其中,互斥条件我们不可以破坏,因为这是互斥锁的基本约束,其他三个条件都可以破坏。

  • 破坏请求和保持条件:线程在请求开始前,一次性申请所有的资源。
  • 破坏不可抢占条件:占用部分资源的线程进一步申请其他资源时,如果申请不到,可以主动释放它占有的资源。
  • 破坏循环等待条件:靠按序申请资源来预防。按某一顺序申请资源,释放资源则反序释放。破坏循环等待条件。

5.1 破坏请求和保持条件

要破坏占用资源所带来的等待,可以一次性申请所有资源,保证同时申请这个操作是在一个临界区中,然后通过一个单独的角色来管理这个临界区。

  • 这个角色有两个很重要的功能,就是同时申请资源和同时释放资源,并且这个角色一定是一个单例。

先定义一个 ApplyLock 类,用来实现统一锁资源的申请,该类中有两个方法:

  • 一个是 applyLock() 方法,用来申请锁;
  • 另一个是free()方法,用来统一释放锁。
public class ApplyLock {
    private List<Object> list = new ArrayList<>();

    public synchronized boolean applyLock(Resource resource1, Resource resource2) {
        if (list.contains(resource1) || list.contains(resource2)) {
            return false;
        } else {
            list.add(resource1);
            list.add(resource2);
            return true;
        }
    }

    public synchronized void free(Resource resource1, Resource resource2) {
        list.remove(resource1);
        list.remove(resource2);
    }
}

修改 Resource 类,定义一个全局唯一的 ApplyLock 实例,然后在 saveResource 中调用 applyLock() 方法和 free() 方法进行统一锁资源的获取和释放。

public class Resource {
    private String name;
    private int count;

    static ApplyLock applyLock = new ApplyLock();

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

    public void staticResource() {
        synchronized (this) {
            System.out.println("static resource");
            count++;
        }
    }

    public void saveResource(Resource resource) {
        applyLock.applyLock(this, resource);
        try {
            System.out.println("save resource:" + Thread.currentThread().getName());
            resource.staticResource();
        } finally {
            applyLock.free(this, resource);
        }
    }
}

由于当前涉及的相关资源都实现了一个统一的锁资源获取和释放,从而打破了请求和保持条件。

5.2 破坏不可抢占条件

破坏不可抢占条件的核心是当前线程能够主动释放尝试占有的资源,这一点 synchronized无法实现。

  • 原因是 synchronized 在申请不到资源时会直接进入阻塞状态,一旦线程被阻塞就无法再释放已经占有的资源。
  • 在 java.util.concurrent 包中的 Lock 锁可以轻松地解决这个问题。Lock 接口中有一个 tryLock() 方法可以尝试抢占资源,如果抢占成功则返回 true,否则返回 false,而且这个过程不会阻塞当前线程。
import java.util.concurrent.locks.ReentrantLock;

public class Resource {
    private String name;
    private int count;

    ReentrantLock lock = new ReentrantLock();

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

    public void staticResource() {
        if (lock.tryLock()) {
            try {
                System.out.println("static resource");
                count++;
            } finally {
                lock.unlock();
            }
        } else {
            System.out.println("抢锁失败");
        }
    }

    public void saveResource(Resource resource) {
        if (lock.tryLock()) {
            try {
                System.out.println("save resource:" + Thread.currentThread().getName());
                resource.staticResource();
            } finally {
                lock.unlock();
            }
        } else {
            System.out.println("抢锁失败");
        }
    }
}

5.3 破坏循环等待条件

破坏循环等待条件的基本思想是:把资源按照某种顺序编号,所有锁资源的申请都按照某种顺序来获取。 比如,可以根据 hashCode 来确定加锁顺序,再根据 hashCode 的大小确定加锁的对象,实现代码如下。

public class Resource {
    private String name;
    private int count;

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

    public void staticResource() {
        synchronized (this) {
            System.out.println("static resource");
            count++;
        }
    }

    public void saveResource(Resource resource) {
        Resource lock = this.hashCode() > resource.hashCode() ? this : resource;
        synchronized (lock) {
            System.out.println("save resource:" + Thread.currentThread().getName());
            resource.staticResource();
        }
    }
}

5.4 经典的哲学家就餐问题

如图所示:

  • 有 5 个哲学家围坐在一张圆桌旁。
  • 每个哲学家都有一个吃饭和思考的状态。
  • 圆桌上放着 5 根筷子(与哲学家数量相同)。
  • 哲学家必须同时拿起两根筷子(左手和右手各一根)才能吃饭,吃完后放下筷子继续思考。

问题描述:如果每个哲学家都拿起左边的筷子并等待右边的筷子,导致所有人相互等待,陷入死锁。

  1. 编号为 0 的哲学家拿到编号为 0 的筷子,并等待编号为 1 的筷子。
  2. 编号为 1 的哲学家拿到编号为 1 的筷子,并等待编号为 2 的筷子。
  3. 编号为 2 的哲学家拿到编号为 2 的筷子,并等待编号为 3 的筷子。
  4. 编号为 3 的哲学家拿到编号为 3 的筷子,并等待编号为 4 的筷子。
  5. 编号为 4 的哲学家拿到编号为 4 的筷子,并等待编号为 0 的筷子。

哲学家就餐问题(死锁):

public class DiningPhilosophers {
    public static class Philosopher implements Runnable {

        private Object leftChopstick;
        private Object rightChopstick;

        public Philosopher(Object leftChopstick, Object rightChopstick) {
            this.leftChopstick = leftChopstick;
            this.rightChopstick = rightChopstick;
        }

        @Override
        public void run() {
            while (true) {
                //思考
                try {
                    doAction("Thinking");
                    //吃饭
                    //拿起左边筷子,拿起右边筷子 放下右边筷子 放下左边筷子

                    synchronized (leftChopstick) {
                        doAction("Picked up left chopstick");
                        synchronized (rightChopstick) {
                            doAction("Picked up right chopstick -eating");

                            doAction("Put down right chopstick");
                        }
                        doAction("Put down left chopstick");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

        private void doAction(String action) throws InterruptedException {
            System.out.println(Thread.currentThread().getName() + " " + action);
            Thread.sleep((long) Math.random() * 10);

        }
    }

    public static void main(String[] args) {
        Philosopher[] philosophers = new Philosopher[5];
        Object[] chopsticks = new Object[philosophers.length];
        for (int i = 0; i < chopsticks.length; i++) {
            chopsticks[i] = new Object();
        }
        for (int i = 0; i < philosophers.length; i++) {
            Object leftChopstick = chopsticks[i];
            Object rightChopstick = chopsticks[(i + 1) % chopsticks.length];

            philosophers[i] = new Philosopher(leftChopstick, rightChopstick);

            new Thread(philosophers[i], "哲学家" + (i + 1) + "号").start();
        }
    }
}

解决的方式有很多,这里我们通过改变一个哲学家拿筷子的顺序,解决死锁问题。

哲学家就餐的换手方案:

public class DiningPhilosophersFix {
    public static class Philosopher implements Runnable {

        private Object leftChopstick;
        private Object rightChopstick;

        public Philosopher(Object leftChopstick, Object rightChopstick) {
            this.leftChopstick = leftChopstick;
            this.rightChopstick = rightChopstick;
        }

        @Override
        public void run() {
            while (true) {
                //思考
                try {
                    doAction("Thinking");
                    //吃饭
                    //拿起左边筷子,拿起右边筷子 放下右边筷子 放下左边筷子

                    synchronized (leftChopstick) {
                        doAction("Picked up left chopstick");
                        synchronized (rightChopstick) {
                            doAction("Picked up right chopstick -eating");

                            doAction("Put down right chopstick");
                        }
                        doAction("Put down left chopstick");
                    }
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }

        private void doAction(String action) throws InterruptedException {
            System.out.println(Thread.currentThread().getName() + " " + action);
            Thread.sleep((long) Math.random() * 10);

        }
    }

    public static void main(String[] args) {
        Philosopher[] philosophers = new Philosopher[5];
        Object[] chopsticks = new Object[philosophers.length];
        for (int i = 0; i < chopsticks.length; i++) {
            chopsticks[i] = new Object();
        }
        for (int i = 0; i < philosophers.length; i++) {
            Object leftChopstick = chopsticks[i];
            Object rightChopstick = chopsticks[(i + 1) % chopsticks.length];

            if (i == philosophers.length - 1) {
                philosophers[i] = new Philosopher(rightChopstick, leftChopstick);
            } else {
                philosophers[i] = new Philosopher(leftChopstick, rightChopstick);

            }
            new Thread(philosophers[i], "哲学家" + (i + 1) + "号").start();
        }
    }
}

6. 实际工程中如何有效避免死锁

  1. 设置超时时间:
    • Lock 的 tryLock(long timeout, TimeUnit unit)
    • synchronized 不具备尝试锁的能力。
  2. 使用最小化锁:减少锁的数量和作用范围,能显著降低死锁发生的概率。
  3. 避免嵌套锁:尽量避免线程在持有一个锁时尝试获取另一个锁。
  4. 使用高级并发工具:SemaphoreCountDownLatchReadWriteLock

7. 其他活性故障

死锁是最常见的活跃性问题,除了死锁之外,还有一些类似的问题,会导致程序无法顺利执行,统称为活跃性问题。

7.1 活锁

什么是活锁:线程处于一种“忙碌但无效”的状态,始终无法完成任务。(俗称内耗)

特点:

  • 程序一直在运行,但是一直在做没有意义的工作。

活锁代码示例:

public class LiveLock {
    static class Spoon {
        private Diner owner; //就餐者

        public synchronized void use() {
            System.out.printf("%s has eaten!", owner.name);
        }

        public Spoon(Diner owner) {
            this.owner = owner;
        }

        public Diner getOwner() {
            return owner;
        }

        public void setOwner(Diner owner) {
            this.owner = owner;
        }

    }

    static class Diner {
        private String name;
        private boolean isHungry;

        public Diner(String name) {
            this.name = name;
            isHungry = true;
        }

        public void eatWith(Spoon spoon, Diner spouse) {
            while (isHungry) { //只有饿的情况下才能进来

                //问题在此处:一直再谦让
                if (spouse.isHungry) {
                    System.out.println(name + ": 亲爱的" + spouse.name + "你先吃吧");
                    spoon.setOwner(spouse);
                    continue;
                }

                spoon.use();
                isHungry = false;
                System.out.println(name + ": 我吃好了");
                spoon.setOwner(spouse);
            }
        }
    }

    public static void main(String[] args) {
        Diner husband = new Diner("牛郎");
        Diner wife = new Diner("织女");

        Spoon spoon = new Spoon(husband);

        new Thread(new Runnable() {
            @Override
            public void run() {
                husband.eatWith(spoon, wife);
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                wife.eatWith(spoon, husband);
            }
        }).start();
    }
}

打印结果:

牛郎: 亲爱的织女你先吃吧
织女: 亲爱的牛郎你先吃吧
牛郎: 亲爱的织女你先吃吧
织女: 亲爱的牛郎你先吃吧
牛郎: 亲爱的织女你先吃吧
织女: 亲爱的牛郎你先吃吧
牛郎: 亲爱的织女你先吃吧
织女: 亲爱的牛郎你先吃吧
牛郎: 亲爱的织女你先吃吧
织女: 亲爱的牛郎你先吃吧
...

解决:以太网的指数退避算法,加入随机因素。

public class LiveLockFix {
    static class Spoon {
        private Diner owner; //就餐者

        public synchronized void use() {
            System.out.printf("%s has eaten!", owner.name);
        }

        public Spoon(Diner owner) {
            this.owner = owner;
        }

        public Diner getOwner() {
            return owner;
        }

        public void setOwner(Diner owner) {
            this.owner = owner;
        }

    }

    static class Diner {
        private String name;
        private boolean isHungry;

        public Diner(String name) {
            this.name = name;
            isHungry = true;
        }

        public void eatWith(Spoon spoon, Diner spouse) {
            while (isHungry) { //只有饿的情况下才能进来
                Random random = new Random();
                //问题在此处:一直再谦让
                if (spouse.isHungry && random.nextInt(10) < 9) {
                    System.out.println(name + ": 亲爱的" + spouse.name + "你先吃吧");
                    spoon.setOwner(spouse);
                    continue;
                }

                spoon.use();
                isHungry = false;
                System.out.println(name + ": 我吃好了");
                spoon.setOwner(spouse);
            }
        }
    }

    public static void main(String[] args) {
        Diner husband = new Diner("牛郎");
        Diner wife = new Diner("织女");

        Spoon spoon = new Spoon(husband);

        new Thread(new Runnable() {
            @Override
            public void run() {
                husband.eatWith(spoon, wife);
            }
        }).start();
        new Thread(new Runnable() {
            @Override
            public void run() {
                wife.eatWith(spoon, husband);
            }
        }).start();
    }
}

活锁的解决方法:

  1. 增加随机性:通过引入随机的等待时间(如使用随机退避算法),避免线程/进程按照相同的模式重复操作。
  2. 设置重试次数或超时:为线程的尝试次数或时间限制设置一个阈值。如果超过限制,则采用其他策略,如强制退出或降级处理。

7.2 饥饿

线程饥饿问题其实指的公平性问题。是指某个线程因无法获取所需资源而无法执行,一直处于等待状态的情况。

饥饿代码示例:

public class StarvationExample {
    private static final Object lock = new Object();

    public static void main(String[] args) {
        Thread highPriorityThread = new Thread(() -> {
            synchronized (lock) {
                while (true) {
                    System.out.println("High priority thread is running...");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });

        Thread lowPriorityThread = new Thread(() -> {
            synchronized (lock) {
                while (true) {
                    System.out.println("Low priority thread is running...");
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        });

        highPriorityThread.setPriority(Thread.MAX_PRIORITY);
        lowPriorityThread.setPriority(Thread.MIN_PRIORITY);

        highPriorityThread.start();
        lowPriorityThread.start();
    }
}

问题解析:

  • 上述代码中,高优先级线程(highPriorityThread)由于持有 lock 锁资源,它可能会导致低优先级线程(lowPriorityThread)一直无法执行,从而出现线程饥饿的现象。

线程饥饿原因:

  1. 资源分配不均: 如果一个线程的优先级一直较低,而系统的调度策略总是优先执行高优先级的线程,那么低优先级线程就可能一直得不到执行的机会,从而发生饥饿。
  2. 线程被无限阻塞:当获得锁的线程需要执行无限时间长的操作时(比如 IO 或者无限循环),那么后面的线程将会被无限阻塞,导致被饿死。

饥饿的解决方法:

  1. 设置合适的线程优先级
  2. 使用公平性调度算法

8. 总结

  1. 死锁
  • 特点:两个或多个线程(进程)相互等待对方释放资源,导致所有线程都无法继续执行。
  • 解决方法:避免一个线程持有多个资源的情况,或使用超时机制,如果一个线程在一定时间内没能获得锁,就放弃等待。
  1. 活锁
  • 特点:线程仍然在运行,但由于不断地响应对方,始终没有实际进展。
  • 解决方法:为避免活锁,可以设置超时机制,或者使用协调机制来避免线程之间过度的反应。
  1. 饥饿
  • 特点:线程无法获得执行机会,但其他线程仍然在运行,造成某些线程得不到资源。
  • 解决方法:使用公平锁或合理的优先级策略,确保每个线程都有机会执行,不会被长时间忽略。

以上就是Java程序死锁问题定位与解决方法的详细内容,更多关于Java程序死锁的资料请关注脚本之家其它相关文章!

相关文章

  • 使用Idea简单快速搭建springcloud项目的图文教程

    使用Idea简单快速搭建springcloud项目的图文教程

    这篇文章主要介绍了使用Idea简单快速搭建springcloud项目,本文通过图文并茂的形式给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2021-01-01
  • SpringBoot框架集成ElasticSearch实现过程示例详解

    SpringBoot框架集成ElasticSearch实现过程示例详解

    这篇文章主要为大家介绍了SpringBoot如何集成ElasticSearch的实现过程示例详解,有需要的朋友可以借鉴参考下,希望能够有所帮助,祝大家多多进步
    2021-11-11
  • Java并发编程之LockSupport类详解

    Java并发编程之LockSupport类详解

    LockSupport是一种线程阻塞工具,它可以在线程内任意位置让线程阻塞.接下来就带着大家详细了解一下LockSupport类,,需要的朋友可以参考下
    2021-05-05
  • BeanDefinition基础信息讲解

    BeanDefinition基础信息讲解

    今天小编就为大家分享一篇关于BeanDefinition基础信息讲解,小编觉得内容挺不错的,现在分享给大家,具有很好的参考价值,需要的朋友一起跟随小编来看看吧
    2019-02-02
  • Java异常(Exception)处理以及常见异常总结

    Java异常(Exception)处理以及常见异常总结

    在《Java编程思想》中这样定义异常,阻止当前方法或作用域继续执行的问题,虽然java中有异常处理机制,但是要明确一点,决不应该用"正常"的态度来看待异常,这篇文章主要给大家介绍了关于Java异常(Exception)处理以及常见异常的相关资料,需要的朋友可以参考下
    2021-10-10
  • Java中断一个线程操作示例

    Java中断一个线程操作示例

    这篇文章主要介绍了Java中断一个线程操作,结合实例形式分析了java中断线程相关的interrupt()、isInterrupted()及interrupted()函数使用技巧,需要的朋友可以参考下
    2019-10-10
  • java 使用策略模式操作JDBC数据库

    java 使用策略模式操作JDBC数据库

    这篇文章主要介绍了java 使用策略模式操作JDBC数据库的相关资料,这里提供实例实现对JDBC数据库的操作增删改查的功能,需要的朋友可以参考下
    2017-08-08
  • java的三种随机数生成方式

    java的三种随机数生成方式

    主要介绍了java的三种随机数生成方式的实现方法,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,,需要的朋友可以参考下
    2021-07-07
  • java注解结合aspectj AOP进行日志打印的操作

    java注解结合aspectj AOP进行日志打印的操作

    这篇文章主要介绍了java注解结合aspectj AOP进行日志打印的操作,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2021-02-02
  • Google Kaptcha 框架实现登录验证码功能(SSM 和 SpringBoot)

    Google Kaptcha 框架实现登录验证码功能(SSM 和 SpringBoot)

    这篇文章主要介绍了Google Kaptcha 实现登录验证码(SSM 和 SpringBoot)功能,本文通过实例代码给大家介绍的非常详细,具有一定的参考借鉴价值,需要的朋友可以参考下
    2018-12-12

最新评论