Java多线程中常见死锁问题及解决方案

在编写Java多线程代码的时候,很难避免会出现线程安全问题,在线程安全问题中也有一个很常见的现象就是死锁现象。今天我们就来聊一聊Java中的死锁问题,以及如何避免死锁问题。本次知识点讲解建立在大家已经知道“锁”🔒(synchronized)的概念。

目录

🍈常见的容易出现死锁的代码及解决办法

1.一个锁对象,在一个线程中被加锁两次

2.请求和保持

3.循环依赖

🍌死锁的四个必要性 


🍈常见的容易出现死锁的代码及解决办法

1.一个锁对象,在一个线程中被加锁两次

🍉错误代码示例

public class Demo10 {
    public static Object locker=new Object();
    public static void main(String[] args) {
        Thread t1=new Thread(()->{
            synchronized (locker){ //①
                synchronized (locker){ //②
                    System.out.println("Hello world");
                }
            }
        });

        //开启线程
        t1.start();
    }
}

结果显示:

🚩错误原因:首先可以看到在上面代码的注释①,此时注释①这里已经拿到locker,并上了锁,当执行到注释②的时候,由于注释②此时的锁对象也是locker,由于锁对象相撞,此时①想要执行完需要执行到②才能释放锁,但是②如果想要执行,需要①执行完释放锁,此时便产生了死锁问题。注意这里理论上来说应该会产生死锁,但是结果会打印出"hello world"

👊打印结果解释:因为Java中synchronized是“可重入锁”,可重入锁是什么意思呢?如果在同一个线程中对同一个锁对象加锁多次结果不会死锁,而是会“放权”(放弃执行的权力),这是因为在每次上锁之后,在锁的内部都会记录当前持有锁的线程,如果下一次被加锁是在同一个线程中,此时第二次加锁会视为不加锁,直接放行。

2.请求和保持

🍉错误代码示例

public class Demo11 {
    //死锁代码2
    public static Object locker1=new Object();
    public static Object locker2=new Object();
    public static void main(String[] args) {
        Thread t1=new Thread(()->{
            synchronized (locker1) {
                System.out.println("t1 拿到锁1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (locker2) {
                    System.out.println("t1 拿到锁2");
                }
            }
        });
        Thread t2=new Thread(()->{
            synchronized (locker2) {
                System.out.println("t2 拿到锁2");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (locker1) {
                    System.out.println("t2 拿到锁1");
                }
            }
        });

        //开启线程
        t1.start();
        t2.start();
    }
}

结果显示:

wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==

🚩错误原因:在代码中,我们可以看到当t1和t2线程均调用start()方法之后,此时主线程,t1线程,t2线程三个的调度执行顺序是不确定的,假设现在当代码执行到“t1 拿到锁1”的时候,注意此时会sleep,sleep的时候t1线程会主动放弃CPU的执行权,此时现在执行权到了t2的手上,t2执行到了“t2 拿到了锁2”,当t2执行到sleep的时候,此时的执行权又到了t1手上,现在t1尝试去拿锁2,但是锁2已经被t2线程拿到了,那么此时t1线程就得阻塞,但是t2线程执行完又需要锁1,但是t1线程现在正处于阻塞状态,于是就出现了,t1需要t2来解除阻塞,t2需要t1来解除阻塞,它们俩就你不让我,我不让你,最终导致谁也不让谁,都不能通过。

🍭解决办法:由于这个死锁问题的产生在于,你不让我我不让你,如果我们现在不让这种现象出现等锁1/锁2释放了再去拿锁1/锁2不久解决了吗?结果归结一句话就是 "我需要你的时候,你刚好在"。

🍭解决代码

public class Demo12 {
    public static Object locker1=new Object();
    public static Object locker2=new Object();
    public static void main(String[] args) {
        Thread t1=new Thread(()->{
            synchronized (locker1) {
                System.out.println("t1 拿到锁1");
            }
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (locker2) {
                    System.out.println("t1 拿到锁2");
                }

        });
        Thread t2=new Thread(()->{
            synchronized (locker2) {
                System.out.println("t2 拿到锁2");
            }
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
                synchronized (locker1) {
                    System.out.println("t2 拿到锁1");
                }

        });

        //开启线程
        t1.start();
        t2.start();
    }
}

3.循环依赖

循环依赖问题有一个经典的模型是哲学家干饭问题

🍚分析:有五位哲学家围坐在一张圆形餐桌旁,每位哲学家左右两边各有一只餐叉。哲学家们的生活由交替进行的思考和进餐构成,进餐时他们必须同时拿起左右两边的餐叉。然而,哲学家们从不交谈,因此他们不知道其他哲学家的状态。如果每位哲学家都试图先拿起左边的餐叉,然后再尝试拿起右边的餐叉,那么在没有适当同步机制的情况下,就可能发生死锁——每位哲学家都拿着一只餐叉等待另一只,而另一只餐叉却被其他哲学家持有,导致无人能够进餐。

🍉错误代码示例

{
    public static Object locker1=new Object();
    public static Object locker2=new Object();
    public static Object locker3=new Object();
    public static Object locker4=new Object();
    public static void main(String[] args) {
        Thread t1=new Thread(()->{
            synchronized (locker1){
                synchronized (locker2){
                    synchronized (locker3){
                        synchronized (locker4){

                        }
                    }
                }
            }
        });
        Thread t2=new Thread(()->{
            synchronized (locker2){
                synchronized (locker3){
                    synchronized (locker4){
                        synchronized (locker1){

                        }
                    }
                }
            }
        });
        Thread t3=new Thread(()->{
            synchronized (locker3){
                synchronized (locker4){
                    synchronized (locker1){
                        synchronized (locker2){

                        }
                    }
                }
            }
        });
        Thread t4=new Thread(()->{
            synchronized (locker4){
                synchronized (locker1){
                    synchronized (locker2){
                        synchronized (locker3){

                        }
                    }
                }
            }
        });

        //开启线程
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

🍭解决办法:此时我们给哲学家编号,同时给叉子编号(如下图)。

由上可以看出,如果我们给多个哲学家和叉子进行一个编号,并且规定使用的顺序,这种方法破坏了死锁循环,从而避免了死锁的发生,如下代码。

🍭解决代码

//代码主要逻辑的展示
public class Demo13 {
    public static Object locker1=new Object();
    public static Object locker2=new Object();
    public static Object locker3=new Object();
    public static Object locker4=new Object();
    public static void main(String[] args) {
        Thread t1=new Thread(()->{
            synchronized (locker1){
                synchronized (locker2){
                    synchronized (locker3){
                        synchronized (locker4){
                            
                        }
                    }
                }
            }
        });
        Thread t2=new Thread(()->{
            synchronized (locker1){
                synchronized (locker2){
                    synchronized (locker3){
                        synchronized (locker4){

                        }
                    }
                }
            }
        });
        Thread t3=new Thread(()->{
            synchronized (locker1){
                synchronized (locker2){
                    synchronized (locker3){
                        synchronized (locker4){

                        }
                    }
                }
            }
        });
        Thread t4=new Thread(()->{
            synchronized (locker1){
                synchronized (locker2){
                    synchronized (locker3){
                        synchronized (locker4){

                        }
                    }
                }
            }
        });
        
        //开启线程
        t1.start();
        t2.start();
        t3.start();
        t4.start();
    }
}

不难看出,上面的代码有顺序的依次上锁,这样不会使代码出现上面的死锁问题。

🍌死锁的四个必要性 

❗❗❗important

①:锁使互斥的(t1线程拿了锁,别的线程就只能阻塞等待,只有等t1线程执行完任务释放锁)

②:锁使不可抢占的(当t1线程拿到了锁,别的线程只能阻塞等待,不能强抢线程)

③:请求和保持(t1线程占有锁1,去拿锁2,此时锁2已经被t2拿走了,且t2需要锁1)

④:循环依赖/循环等待……(多个线程获取锁的过程,存在循环的等待,阻塞)

以上就是今天分享的全部内容啦,希望对大家有帮助~~ 有问题和指导欢迎亲临评论区~~

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值