如何解决Java多线程死锁问题

 更新时间:2021年05月13日 09:32:23   作者:Putarmor  
死锁是一个很严重的、必须要引起重视的问题,本文主要介绍了死锁的定义,解决方法和面试会遇到的问题,感兴趣的可以了解一下

死锁问题

死锁定义

多线程编程中,因为抢占资源造成了线程无限等待的情况,此情况称为死锁

死锁举例

注意:线程和锁的关系是:一个线程可以拥有多把锁,一个锁只能被一个线程拥有。

当两个线程分别拥有一把各自的锁之后,又尝试去获取对方的锁,这样就会导致死锁情况的发生,具体先看下面代码:

/**
 * 线程死锁问题
 */
public class DeadLock {
    public static void main(String[] args) {
        //创建两个锁对象
        Object lock1 = new Object();
        Object lock2 = new Object();

        //创建子线程
        /*
        线程1:①先获得锁1 ②休眠1s,让线程2获得锁2 ③线程1尝试获取锁2 线程2同理
         */
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                //线程1业务逻辑
                synchronized(lock1){
                    System.out.println("线程1得到了锁子1");
                    try {
                        //休眠1s,让线程2先得到锁2
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("线程1尝试获取锁2...");
                    synchronized(lock2){
                        System.out.println("线程1获得了锁2!");
                    }
                }
            }
        },"线程1");


        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                //线程2业务逻辑
                synchronized(lock2){
                    System.out.println("线程2得到了锁子2");
                    try {
                //休眠1s,让线程1先得到锁1;因为线程是并发执行我们不知道谁先执行
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("线程2尝试获取锁1...");
                    synchronized(lock1){
                        System.out.println("线程2获得了锁1");
                    }
                }
            }
        },"线程2");
        thread1.start();
        thread2.start();
    }
}

程序运行结果如下:

在这里插入图片描述 

可以看出,线程1尝试获取了锁2,线程2尝试获取了锁1,但是二者并没有获取到对方的锁;这就发生了所谓的“死锁”!

如何排查死锁

想要排查死锁具体细节,可以通过三个工具(位于jdk安装路径bin目录)去排查,现在就给大家介绍一下:

1.jconsole

在这里插入图片描述

可以看出,线程1和线程2发生了死锁,死锁发生的位置一目了然

2.jvisualvm

在这里插入图片描述

可以看出,发生了死锁,线程1和线程2尝试获取的锁是对方的锁。

3.jmc

在这里插入图片描述

可以看出,同样检测出了死锁情况
无论是用哪个工具排查死锁情况都是OK的。

死锁发生的条件

1.互斥条件(一个锁只能被一个线程占有,当一个锁被一个线程持有之后,不能再被其他线程持有);
2.请求拥有(一个线程拥有一把锁之后,又去尝试请求拥有另外一把锁);可以解决
3.不可剥夺(一个锁被一个线程占有之后,如果该线程没有释放锁,其他线程不能强制获得该锁);
4.环路等待条件(多线程获取锁时形成了一个环形链)可以解决

怎么解决死锁问题?

环路等待条件相对于请求拥有更容易实现,那么通过破坏环路等待条件解决死锁问题
破坏环路等待条件示意图:

在这里插入图片描述

针对于上面死锁举例中代码,解决死锁,具体看下面代码:

/**
 * 线程死锁问题
 */
public class DeadLock {
    public static void main(String[] args) {
        //创建两个锁对象
        Object lock1 = new Object();
        Object lock2 = new Object();

        //创建子线程
        /*
        线程1:①先获得锁1 ②休眠1s,让线程2获得锁2 ③线程1尝试获取锁2 线程2同理
         */
        Thread thread1 = new Thread(new Runnable() {
            @Override
            public void run() {
                //线程1业务逻辑
                synchronized(lock1){
                    System.out.println("线程1得到了锁子1");
                    try {
                        //休眠1s,让线程2先得到锁2
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("线程1尝试获取锁2...");
                    synchronized(lock2){
                        System.out.println("线程1获得了锁2!");
                    }
                }
            }
        },"线程1");


        Thread thread2 = new Thread(new Runnable() {
            @Override
            public void run() {
                //线程2业务逻辑
                synchronized(lock2){
                    System.out.println("线程2得到了锁子2");
                    try {
                //休眠1s,让线程1先得到锁1;因为线程是并发执行我们不知道谁先执行
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    System.out.println("线程2尝试获取锁1...");
                    synchronized(lock1){
                        System.out.println("线程2获得了锁1");
                    }
                }
            }
        },"线程2");
        thread1.start();
        thread2.start();
    }
}

程序运行结果如下:

在这里插入图片描述

可以看出,通过破坏环路等待条件完美解决了死锁问题

线程通讯机制(wait/notify/notifyAll)

定义

线程通讯机制:一个线程的动作可以让另外一个线程感知到,这就是线程通讯机制。
wait():让当前线程进入休眠等待状态;
notify():唤醒当前对象上的休眠等待线程;
notifyAll():唤醒当前对象上的所有休眠等待线程。

相关面试重点

面试问题:
1.wait()使用时为什么需要加锁?
因为wait()必须在同步方法或者同步块中使用,也就是说wait()需要配合加锁一起使用(比如synchronized或Lock),调用对象调用wait()如果没有适当的锁,就会引发异常,因此说wait()使用时需要加锁。
2.wait()使用为什么要释放锁?
wait()是Objetc类中一个实例方法,默认是不传任何值的,不传值的时候表示让当前线程处于永久休眠等待状态,这样会造成一个锁被一个线程长时间一直拥有,为了避免这种问题的发生,使用wait()后必须释放锁。

wait()/notify()/notifyAll()使用时注意事项:
使用这三个方法时都必须进行加锁;
2.加锁的对象和调用wait()/notify()/notifyAll()对象必须是同一个对象;
3.一组wait()/notify()/notifyAll()必须是同一个对象;
4.notify()只能唤醒当前对象上的一个休眠等到线程;而notifyAll()可以唤醒当前对象上的所有休眠等待线程。

sleep(0)和wait(0)的区别:
1.sleep()是Thread类中一个静态方法,wait()是Object类中一个普通的成员方法;
2.sleep(0)会立即触发一次CPU的抢占执行,wait(0)会让当前线程无限休眠等待下去。

wait()和sleep()的区别:
相同点:
1.都会让当前线程进行休眠等待;
2.使用二者时都需处理InterruptedException异常(try/catch)。
不同点:
1.wait()是Object中普通成员方法,sleep是Thread中静态方法;
2.wait()使用可以不穿参数,sleep()必须传入一个大于等于0的参数;
3.wait()使用时必须配合加锁一起使用,sleep()使用时不需要加锁;
4.wait()使用时需要释放锁,如果sleep()加锁后不会释放锁;
5.wait()会让当前线程进入WAITING状态(默认没有明确的等待时间,当被别的线程唤醒或者wait()传参后超过等待时间量自己唤醒,将进入就绪状态),sleep()会让当前线程进入TIMED_WAITING状态(有明确的结束等待时间,但是这是死等的方式,休眠结束后进入就绪状态)。

*为什么wait()处于Object中而不是Thread中?(有点绕 我有点懵了…)
wait()的调用必须进行加锁和释放锁操作,而锁是属于对象级别非线程级别,也就是说锁针对于对象进行操作而不是线程;而线程和锁是一对多的关系,一个线程可以拥有多把锁,而一个线程只能被一个线程拥有,为了灵活操作,就将wait()放在Object中。

LockSupport

LockSupport是对wait()的升级,无需加锁也无需释放锁;

LockSupport.park()让线程休眠,和wait()一样会让线程进入WAITING状态;LockSupport.unpark()唤醒线程,可以唤醒对象上指定的休眠等待线程;(优势)

LockSupport与wait()区别

wait()与LockSupport的区别:

相同点:
1.二者都可以让线程进入休眠等待状态;
2.二者都可以传参或者不传参,让线程都会进入到WAITING状态。
不同点:
1.wait()需要配合加锁一起使用,LockSupport无需加锁;
2.wait()只能唤醒对象的随机休眠线程和全部线程,LockSupport可以唤醒对象的指定休眠线程。

到此这篇关于如何解决Java多线程死锁问题的文章就介绍到这了,更多相关Java多线程死锁内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

  • IDEA 中使用 Hudi的示例代码

    IDEA 中使用 Hudi的示例代码

    这篇文章主要介绍了IDEA 中使用 Hudi的相关知识,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2022-06-06
  • Spring动态代理实现日志功能详解

    Spring动态代理实现日志功能详解

    这篇文章主要为大家详细介绍了Spring动态代理实现日志功能,文中示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-08-08
  • 深入解析HashMap的put方法

    深入解析HashMap的put方法

    在Java集合中,HashMap的重要性不言而喻,作为一种存储键值对的数据结构,它在日常开发中有着非常多的应用场景,也是面试中的高频考点,本篇文章就来分析一下HashMap集合中的put方法
    2022-01-01
  • 解决若依pageHelper在动态切换数据源问题

    解决若依pageHelper在动态切换数据源问题

    这篇文章主要介绍了解决pageHelper在动态切换数据源问题,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-01-01
  • Java Feign微服务接口调用方法详细讲解

    Java Feign微服务接口调用方法详细讲解

    现如今微服务架构十分流行,而采用微服务构建系统也会带来更清晰的业务划分和可扩展性。java如果使用微服务就离不开springcloud,我这里是把服务注册到nacos上,各个服务之间的调用使用feign
    2023-01-01
  • MapStruct到底是什么?

    MapStruct到底是什么?

    今天给大家带来的是关于Java的相关知识,文中围绕MapStruct到底是什么展开,文中有非常详细的解释及代码示例,需要的朋友可以参考下
    2021-06-06
  • java 用递归获取一个目录下的所有文件路径的小例子

    java 用递归获取一个目录下的所有文件路径的小例子

    还是日志的问题,log4j生成的日志文件,自动保存到月份所在的文件夹中,需要获取到所有的日志文件,包括文件夹
    2013-09-09
  • map实现按value升序排序

    map实现按value升序排序

    map内部是按照hash算法存储的,但如果能对map排序在某些时候还是有用的,下面实现对map按照value升序排序,实现对map按照key排序,大家参考使用吧
    2014-01-01
  • Java实现格式化打印慢SQL日志的方法详解

    Java实现格式化打印慢SQL日志的方法详解

    不管我们使用何种语言开发,一旦程序发生异常,日志是一个很重要的数据,下面这篇文章主要给大家介绍了关于Java实现格式化打印慢SQL日志的相关资料,需要的朋友可以参考下
    2022-10-10
  • Spring超详细讲解注解开发

    Spring超详细讲解注解开发

    Spring是轻代码而重配置的框架,配置比较繁重,影响开发效率,所以注解开发是一种趋势。本文将通过示例为大家详细讲讲Spring如何实现注解开发,感兴趣的可以学习一下
    2022-08-08

最新评论