JavaEE 第5节 死锁产生的原因

目录

一、死锁场景

场景1:1个线程1个锁

场景2:2个线程2个锁

场景3:N个线程M个锁

二、出现死锁的四个必要条件

1)锁的互斥性(Mutual Exclusion)

2)锁的不可抢占性(Non-preemption)

3)请求保持(Hold and Wait)

4)循环等待(Circular Wait)

三、避免死锁的方式

1.锁的互斥性(不可干预)

2.锁的不可抢占性(不可干预)

3.请求锁时,不允许持有锁

4.打破循环依赖


一、死锁场景

为了能够加深对死锁产生原因的理解,我们先来看看产生死锁的几个经典场景:

场景1:1个线程1个锁

对这个线程进行重复上锁:

public class Threads {
    static int count = 0;

    //实现加锁
    private synchronized static void add() {
        count++;
    }
public static Object object=new Object();
    public static void main(String[] args) throws InterruptedException {
        
        Thread t=new Thread(()->{

            //第一次上锁
            synchronized (object){
                //程序会永远停留在这里
                /*第二次上锁,需要等待第二次解锁,但是第二次解锁需要等待第二次上锁成功。
                * */
                //第二次上锁
                synchronized(object){
                    
                }
            }
        });
        t.start();
        t.join();

    }

}

注意:

在JAVA的synchronized关键字中,不会出现这中情况,因为synchronized关键字会自动识别,只对同一个线程的同一个对象上锁一次。


那么synchronized是怎么完成这一操作的呢?

底层使用的是引用计数:

这样,不论加几次锁,实际的锁只有一个。

场景2:2个线程2个锁

我先用一个形象的例子说明:

假如有两个小朋友A和B分别在一个独木桥的两端,他们都要过这个一个独木桥。

当他们在独木桥相遇的时候,出现了这样一个情况,两个人都相互谦让:

最后结果就是两个人都停留在那里,都走不了。

用代码演示:

public class Threads {
    static int count = 0;

    //实现加锁
    private synchronized static void add() {
        count++;
    }
public static Object object1=new Object();
    public static Object object2=new Object();
    public static void main(String[] args) throws InterruptedException {

            Thread threadA=new Thread(()->{
                synchronized (object1){
                    System.out.println("线程A对1上锁");

                    try {
                        Thread.sleep(1000);//休眠确保线程B对1 2都上锁,然后在执行A线程接下来的程序
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    synchronized(object2){
                        System.out.println("线程A在对2上锁");
                    }

                }


            });

            Thread threadB=new Thread(()->{
               synchronized(object2){
                   System.out.println("线程B对2上锁");

                   synchronized(object1){
                       System.out.println("线程B对1上锁");
                   }
               }
            });


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

}

运行结果:

打印完这两句话后进入死循环。

注意:

出现这种死锁必须保证这两个线程是在持有一个锁的基础上,又去加另一个锁,比如A线程在上了1锁的前提下(synchronized代码块内),有取加了2的锁:

线程B,也必须如此:

        

场景3:N个线程M个锁

场景三是一个很经典的模型:

假如说下面五位都要吃到桌上的食物,每个人需要用一双筷子才行:

上面的五位哥哥就是五个线程,五支筷子就是五个不同对象的锁。

通常情况下,都能吃到里面的鸡汤(一个哥哥用完一双筷子,把筷子留给其他哥哥用),但是有时候程序运行时,会出现这样一个特殊的情况:

也就是他们同时拿其同一侧的一只筷子的时候,每一位哥哥都拿了一支筷子,但是每一个哥哥都尝不到里面的鸡汤,因为必须持有一双筷子才能尝到鸡汤的鸡肉,于是每位各个都一直干等着,这样就形成了死锁。

二、出现死锁的四个必要条件

1)锁的互斥性(Mutual Exclusion)

可以把锁想象成一个很小的单间,如果有多个线程加了这把锁,那么同一时刻只能有一个线程能够进到这个房间,既这把锁只能被一个线程持有并使用,保证持有这把锁的线程对资源独占访问,从而避免线程安全问题

这是锁的基本特性,无法干预

2)锁的不可抢占性(Non-preemption)

还是刚才的单间例子,在一个线程抢到到这个小房间的时候(持有这把锁),其他线程不能把这个线程强制拖出来自己占有

这是锁的基本特性,通常无法干预

3)请求保持(Hold and Wait)

最典型的例子就是场景1和场景2了。

与代码结构有关,可干预。

在持有一把锁且不释放的情况下,又去拿锁。

4)循环等待(Circular Wait)

顾名思义,线程之间在相互等待解锁,形成一个逻辑上的死循环。

与代码结构有关,可干预。

三、避免死锁的方式

想要解决死锁问题,只需要破坏一下四个条件中的其中一个就能成功。

1.锁的互斥性(不可干预)

锁的互斥性(Mutual Exclusion)是锁的基本特性,它是保证线程资源独占访问的关键,是无法干预的。

2.锁的不可抢占性(不可干预)

锁的不可抢占性(Non-preemption)也是锁的基本特性,它保证线程在持有资源的时候不会被其他线程打断,保证数据的一致性,所以也是不可干预的。

3.请求锁时,不允许持有锁

请求保持(Hold and Wait):这个条件我们是可以干预的。

对典型的就是上面2个线程2把锁这个例子,倘若两个线程想要拿另一把锁的时候,把当前的锁释放掉,再去拿另一把锁,死锁就不会发生:

public class Threads {
    static int count = 0;

    //实现加锁
    private synchronized static void add() {
        count++;
    }
    public static Object object1=new Object();
    public static Object object2=new Object();
    public static void main(String[] args) throws InterruptedException {

        Thread threadA=new Thread(()->{
            synchronized (object1){
                System.out.println("线程A对1上锁");

                try {
                    Thread.sleep(1000);//休眠确保线程B对1 2都上锁,然后在执行A线程接下来的程序
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }

            //出了object1对象的锁,再去压object2的锁,其他代码不变
            synchronized(object2){
                System.out.println("线程A在对2上锁");
            }


        });

        Thread threadB=new Thread(()->{
            synchronized(object2){
                System.out.println("线程B对2上锁");

                synchronized(object1){
                    System.out.println("线程B对1上锁");
                }
            }
        });


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

}

运行结果:

注意:

这个解决方案不是万能的,因为有时候代码就是要写成保持请求的状态。

4.打破循环依赖

产生循环依赖,这个当然是可以干预的。想办法打破循环条件即可。

最简单高效的方式就是给锁编号,比如上文谈到的N个线程M把锁:

我们把五枝筷子(锁)进行编号1、2、3、4、5

规定,每一位各个拿筷子的时候只能先拿起编号小的筷子后拿起编号大的筷子

这是我们发现,循环依赖实际上已经打破了,因为因为一号哥哥不会去大号(5号)筷子,所以五号哥哥可以拿到5号筷子,等五号哥哥炫完鸡汤后,他会把4、5号筷子都放下,然后三号哥哥就可以开炫了,以此类推直到所有线程完成任务。


另外银行家算法也可以解决循环依赖的问题,但是本身这个算法就很复杂,容易出bug,所以这里就不做过多讨论了,感兴趣的同学可以自己去查阅学习。

  • 17
    点赞
  • 31
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值