当前位置: 首页 > news >正文

多线程 (进阶) 死锁的成因和解决方案

🎉🎉🎉点进来你就是我的人了
博主主页:🙈🙈🙈戳一戳,欢迎大佬指点!

人生格言:当你的才华撑不起你的野心的时候,你就应该静下心来学习!

欢迎志同道合的朋友一起加油喔🦾🦾🦾
目标梦想:进大厂,立志成为一个牛掰的Java程序猿,虽然现在还是一个🐒嘿嘿
谢谢你这么帅气美丽还给我点赞!比个心


目录

前言

一. 死锁的必要条件

二. Java 多线程中产生死锁的主要原因

1.竞争同一把锁时发生死锁

2.多个锁的嵌套导致死锁

3. 对共享资源的并发访问导致死锁

三. 破除循环等待解决死锁(最有效的一种方式)

四. 死锁解决方案汇总

 1. 避免使用多把锁

 2. 避免嵌套锁

 3. 统一锁的获取顺序

 4. 限制锁的持有时间

 5. 超时等待锁

6.破除循环等待

7. 检测死锁



前言

在Java中,死锁是指两个或多个线程相互等待对方已持有的锁,导致所有线程都被阻塞,无法继续执行的情况。死锁是多线程程序常见的问题之一,如果程序中存在死锁,会导致系统性能下降,甚至崩溃。


一. 死锁的必要条件

死锁是指两个或多个线程无限期地等待对方持有的资源而导致的一种阻塞现象。在 Java 多线程编程中,死锁通常是由于多个线程在竞争资源时出现了相互等待的情况,导致所有线程都无法继续执行,从而产生死锁。

在深入了解死锁的成因之前,需要先了解死锁发生的必要条件,包括以下四个条件:

  1. 互斥使用,即当资源被一个线程使用(占有)时,别的线程不能使用
  2. 不可抢占,资源请求者不能强制从资源占有者手中夺取资源,资源只能由资源占有者主动释放。
  3. 请求和保持,即当资源请求者在请求其他的资源的同时保持对原有资源的占有。
  4. 循环等待,即存在一个等待队列:P1占有P2的资源,P2占有P3的资源,P3占有P1的资源。这样就形成了一个等待环路。

只有当这四个条件同时满足时,才可能发生死锁。如果其中任何一个条件不满足,就不会发生死锁。因此,避免死锁的关键是破坏这四个条件中的至少一个。

例如,可以通过使用同步机制来破坏请求和保持条件,避免进程在请求资源时阻塞并持有已获得的资源。同时,可以使用超时机制来破坏不剥夺条件,避免进程长时间持有资源而不释放。另外,可以使用资源分配图等工具来检测循环等待条件,从而避免出现循环等待的情况。

综上所述,了解死锁的必要条件对于理解死锁的成因和避免死锁非常重要。只有在避免或破坏这些必要条件的基础上,才能有效地避免死锁的发生。

二. Java 多线程中产生死锁的主要原因

 锁的不当使用,包括以下几种情况:

1.竞争同一把锁时发生死锁

如果一个线程对同一把锁,连续加了两次锁,并且该锁还是不可重入锁的时候,就会产生死锁。

2.多个锁的嵌套导致死锁

在 Java 中,如果多个线程在持有一个锁的情况下尝试获取另一个锁,并且在相互等待对方释放锁时,就会出现死锁。这种情况通常是由于多个线程在获取锁的顺序上存在差异,从而导致相互等待。

下面是一个简单的 Java 代码示例,演示了多个锁的嵌套导致死锁的情况:

public class DeadlockDemo {

    private static final Object lock1 = new Object();
    private static final Object lock2 = new Object();

    public static void main(String[] args) {
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lock1) {
                    System.out.println("线程 1 获取了锁 1");
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (lock2) {
                        System.out.println("线程 1 获取了锁 2");
                    }
                }
            }
        });
        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (lock2) {
                    System.out.println("线程 2 获取了锁 2");
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (lock1) {
                        System.out.println("线程 2 获取了锁 1");
                    }
                }
            }
        });
        thread1.start();
        thread2.start();
    }
}

注意到,在这个示例中,有两个线程分别获取两个锁,但是获取锁的顺序不同。这个代码会导致死锁的发生,因为每个线程都在等待另一个线程释放它需要的锁。在这个示例中,线程 1 获取了锁 1,但是需要等待线程 2 释放锁 2;而线程 2 获取了锁 2,但是需要等待线程 1 释放锁 1。这个死锁问题可以通过改变锁的获取顺序来避免。

3. 对共享资源的并发访问导致死锁

在 Java 中,如果多个线程在并发访问共享资源时相互等待对方释放资源,并且在持有资源的情况下等待其他线程释放资源,就会出现死锁。这种情况通常是由于多个线程在访问共享资源时没有使用同步机制,从而导致竞争条件和死锁。

下面是一个简单的 Java 代码示例,演示了对共享资源的并发访问导致死锁的情况:

package test2;

import java.util.ArrayList;
import java.util.List;

public class DeadlockDemo {

    public static void main(String[] args) {
        List<Integer> list1 = new ArrayList<>();
        List<Integer> list2 = new ArrayList<>();

        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (list1) {
                    System.out.println("线程 1 获取了列表 1");
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (list2) {
                        System.out.println("线程 1 获取了列表 2");
                    }
                }
            }
        });

        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                synchronized (list2) {
                    System.out.println("线程 2 获取了列表 2");
                    try {
                        Thread.sleep(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized (list1) {
                        System.out.println("线程 2 获取了列表 1");
                    }
                }
            }
        });

        thread1.start();
        thread2.start();
    }
}

   注意到,在这个代码示例中,有两个线程分别对两个列表进行加锁,但是加锁的顺序是不同的。这个代码会导致死锁的发生,因为每个线程都在等待另一个线程释放它需要的锁。在这个示例中,线程 1 获取了列表 1 的锁,但是需要等待线程 2 释放列表 2 的锁;而线程 2 获取了列表 2 的锁,但是需要等待线程 1 释放列表 1 的锁。这个死锁问题可以通过改变锁的获取顺序来避免。     

三. 破除循环等待解决死锁(最有效的一种方式)

引入案例:

哲学家就餐问题是一个经典的并发编程问题,描述了多个哲学家在围绕圆桌就餐时可能出现的死锁问题。在这个问题中,有五个哲学家坐在一张圆桌周围,每个哲学家需要交替地进行思考和进餐。在圆桌上有个碗和五个筷子,每个哲学家需要同时拿起自己左右两侧的筷子才能够进餐,然后在吃完之后释放筷子。如果所有哲学家都在同时等待对方释放筷子,就会出现死锁。

如果当5个哲学家同时要吃面条,拿起了左手边的筷子,就会出现死锁,因为右手边的筷子被其他哲学家拿走了

下面是一个简单的 Java 代码示例,演示了哲学家就餐问题可能导致死锁的情况:

package test2;

public class DiningPhilosophers {

    private static final int NUM_PHILOSOPHERS = 5;
    private static final Object[] forks = new Object[NUM_PHILOSOPHERS];

    public static void main(String[] args) {
        // 初始化筷子对象
        for (int i = 0; i < NUM_PHILOSOPHERS; i++) {
            forks[i] = new Object();
        }

        // 创建哲学家线程并启动
        for (int i = 0; i < NUM_PHILOSOPHERS; i++) {
            final int philosopher = i;
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    while (true) {
                        // 哲学家思考
                        think(philosopher);
                        // 哲学家拿起左边的筷子
                        synchronized (forks[philosopher]) {
                            System.out.println("哲学家 " + philosopher + " 拿起左边的筷子");
                            // 哲学家拿起右边的筷子
                            synchronized (forks[(philosopher + 1) % NUM_PHILOSOPHERS]) {
                                System.out.println("哲学家 " + philosopher + " 拿起右边的筷子");
                                // 哲学家进餐
                                eat(philosopher);
                            }
                            System.out.println("哲学家 " + philosopher + " 放下右边的筷子");
                        }
                        System.out.println("哲学家 " + philosopher + " 放下左边的筷子");
                    }
                }
            });
            thread.start();
        }
    }

    private static void think(int philosopher) {
        try {
            Thread.sleep((long) (Math.random() * 10000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("哲学家 " + philosopher + " 思考");
    }

    private static void eat(int philosopher) {
        try {
            Thread.sleep((long) (Math.random() * 10000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("哲学家 " + philosopher + " 进餐");
    }
}

在这个示例中,五个哲学家被建模为五个线程,每个哲学家拥有一个编号,从 0 到 4。为了模拟哲学家就餐的过程,每个哲学家都会在思考一段时间之后尝试拿起左右两侧的筷子,然后进餐一段时间,最后释放筷子并继续思考。在拿起筷子和放下筷子的过程中,每个哲学家需要先获取自己左右两侧的筷子,因此可能会出现死锁。例如,如果所有哲学家都同时拿起了自己左边的筷子,就会出现死锁。要解决这个问题,可以使用一些技巧来破坏死锁发生的循环等待:

解决哲学家就餐问题的方法是给筷子排序,让所有哲学家都先拿起编号较小的筷子,然后再拿编号较大的筷子。这样可以避免出现所有哲学家都拿起左边的筷子的情况,从而避免死锁的发生。

下面是一个使用筷子排序解决哲学家就餐问题的 Java 代码示例:

package test3;

public class DiningPhilosophers {

    private static final int NUM_PHILOSOPHERS = 5;
    private static final Object[] forks = new Object[NUM_PHILOSOPHERS];

    public static void main(String[] args) {
        // 初始化筷子对象
        for (int i = 0; i < NUM_PHILOSOPHERS; i++) {
            forks[i] = new Object();
        }

        // 创建哲学家线程并启动
        for (int i = 0; i < NUM_PHILOSOPHERS; i++) {
            final int philosopher = i;
            Thread thread = new Thread(new Runnable() {
                @Override
                public void run() {
                    while (true) {
                        // 哲学家思考
                        think(philosopher);
                        // 拿起左边的筷子
                        synchronized (forks[philosopher]) {
                            System.out.println("哲学家 " + philosopher + " 拿起左边的筷子");
                            // 拿起右边的筷子
                            synchronized (forks[(philosopher + 1) % NUM_PHILOSOPHERS]) {
                                System.out.println("哲学家 " + philosopher + " 拿起右边的筷子");
                                // 哲学家进餐
                                eat(philosopher);
                            }
                            System.out.println("哲学家 " + philosopher + " 放下右边的筷子");
                        }
                        System.out.println("哲学家 " + philosopher + " 放下左边的筷子");
                    }
                }
            });
            thread.start();
        }
    }

    private static void think(int philosopher) {
        try {
            Thread.sleep((long) (Math.random() * 10000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("哲学家 " + philosopher + " 思考");
    }

    private static void eat(int philosopher) {
        try {
            Thread.sleep((long) (Math.random() * 10000));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("哲学家 " + philosopher + " 进餐");
    }
}

在这个示例中,使用了与之前相同的思考、拿起筷子和放下筷子的逻辑。不同之处在于,每个哲学家现在必须先拿起编号较小的筷子,再拿编号较大的筷子,从而避免出现所有哲学家都拿起左边的筷子的情况。这个示例并没有使用任何锁排序算法,而是直接通过代码来保证筷子排序。

注意到在哲学家线程中,先对左边的筷子加锁,再对右边的筷子加锁。这个顺序是非常重要的,因为如果两个哲学家同时想要拿起相邻的筷子,就有可能出现死锁的情况。如果所有哲学家都按照相同的顺序拿起筷子,就可以避免这种情况的发生。

在这个示例中,为了简化代码,直接使用了编号较小的筷子来加锁。这样就可以保证所有哲学家都按照相同的顺序拿起筷子。需要注意的是,这个示例中并没有使用任何锁排序算法,所以如果每个哲学家都同时拿起了左边的筷子,仍然会发生死锁。

总之,通过给筷子排序来解决哲学家就餐问题是一种简单而有效的方法,可以避免死锁的发生。但是,如果实现不当,仍然可能出现死锁的情况。因此,在编写多线程程序时,一定要谨慎地处理锁的使用,以避免出现死锁和其他并发问题。

四. 死锁解决方案汇总

 1. 避免使用多把锁

使用多把锁会增加死锁的概率,因此应该尽量避免使用多把锁。可以考虑使用更高级别的同步工具,例如信号量、读写锁、并发集合等。

 2. 避免嵌套锁

在持有一个锁的情况下,尽量避免获取其他锁,尤其是嵌套锁。如果确实需要嵌套锁,可以考虑使用线程本地变量或者其他的同步工具来避免死锁。

 3. 统一锁的获取顺序

在多线程中使用多把锁时,为了避免死锁,应该尽量保证所有线程获取锁的顺序是一致的。可以按照某种全局的规则来确定锁的获取顺序,例如按照对象的 hash 值来获取锁。

 4. 限制锁的持有时间

持有锁的时间越长,发生死锁的概率就越大。因此,可以考虑限制锁的持有时间,避免线程长时间持有锁而导致其他线程无法获取锁的情况。

 5. 超时等待锁

如果一个线程尝试获取锁时发现已经被其他线程占用,可以设置一个超时时间,超过这个时间就放弃获取锁。这样可以避免线程一直阻塞等待锁而导致死锁。

6.破除循环等待

    6.1 按顺序获取资源

按顺序获取资源是一种比较常见的破除循环等待的方法。如果所有的线程都按照固定的顺序获取资源,那么就不会出现循环等待的情况。

    6.2 统一资源分配器

统一资源分配器是一种能够有效避免死锁的方法。在这种方法中,所有的资源都由一个统一的资源分配器来进行分配和释放,每个线程在需要资源时向资源分配器发出请求,资源分配器根据当前的情况来分配资源。这样就能够避免循环等待和其他死锁问题的发生。

7. 检测死锁

可以定期检测系统中是否存在死锁,并且采取相应的措施来解决死锁问题。例如,可以使用 jstack 工具来查看死锁情况,或者使用死锁检测算法来自动检测死锁。

相关文章:

  • Linux基础知识——基础命令/基础指令
  • c++开发环境安装
  • pycharm连接虚拟机中的spark
  • 解决服务器系统磁盘满了的问题
  • 玩转易知微社区,就差你了
  • 认识数据库管理工具 dbForge Edge,您的多数据库解决方案!
  • 「微报告」智驾芯片收敛“前夜”
  • 代码随想录Day36
  • 二维字符数组与char** 关系 段错误打印
  • 医学图像增强系统的设计_kaic
  • 【Python机器学习】——入门
  • 学习+刷题:239. 滑动窗口最大值
  • 测试老鸟手把手教你python接口自动化测试项目实战演示
  • FB使用入口点函数例子
  • Vue 04 - Vue模板语法
  • 【算法题】2498. 青蛙过河 II
  • 【Java】自定义注解和AOP切面的使用
  • 论文心得笔记
  • 等保部作业
  • ASIC-WORLD Verilog(3)第一个Verilog代码
  • 电加热油锅炉工作原理_电加热导油
  • 大型电蒸汽锅炉_工业电阻炉
  • 燃气蒸汽锅炉的分类_大连生物质蒸汽锅炉
  • 天津市维修锅炉_锅炉汽化处理方法
  • 蒸汽汽锅炉厂家_延安锅炉厂家
  • 山西热水锅炉厂家_酒店热水 锅炉
  • 蒸汽锅炉生产厂家_燃油蒸汽发生器
  • 燃煤锅炉烧热水_张家口 淘汰取缔燃煤锅炉
  • 生物质锅炉_炉
  • 锅炉天然气_天燃气热风炉