Interviewer: Talk about your understanding of deadlock

1. What is deadlock

For example, when we talked about synchronized last time, a thread locks the same object twice consecutively. If there is a blocking wait, it means that the lock is non-reentrant. Such a thread is also called deadlock!

Once the program enters a deadlock, the thread will freeze and cannot continue to perform subsequent work, and the program will have a serious BUG!

The situation of deadlock is very hidden. In the development stage, deadlock may occur inadvertently!

2. Three typical situations of deadlock

2.1 One thread, one lock

A thread repeatedly locks the same object twice. If reentrancy is not supported, deadlock will occur. However, in Java, both synchronized and ReentarntLock (described later) support reentrancy!

So in Java, this will not happen, but it does not rule out that it will happen in other languages.

2.2 Two threads and two locks

Let me give you an example from real life to help you understand better:

One day, Zhang San and Xiao Mei were two terrorists. They went to eat steak. Since Xiao Mei couldn't eat too much, they ordered a steak. The waiter only gave him a knife and fork. At this time, Zhang San She snatched the knife, and Xiaomei snatched the fork. At this time, Xiaomei said to Zhang San, "Give me the knife first, and I will give it to you after taking a bite. Take a bite and give it to you, so neither of them will let the other, so that no one can eat steak!

If the above example is put into programming, for example, Zhang San and Xiao Mei are two threads, Zhang San acquires the lock of object A, and Xiao Mei acquires the lock of object B, but they both try to acquire the lock of the other party!

Expressed in code as follows:

public static void main(String[] args) {
    Object knife = new Object(); //刀
    Object fork = new Object(); //叉
    Thread t1 = new Thread(() -> {
        synchronized (knife) {
            System.out.println("张三拿到刀了!");
            synchronized (fork) {
                System.out.println("张三刀和叉都拿到了!吃牛排!");
            }
        }
    });
    Thread t2 = new Thread(() -> {
        synchronized (fork) {
            System.out.println("小美拿到叉了!");
            synchronized (knife) {
                System.out.println("小美刀和叉都拿到了!吃牛排!");
            }
        }
    });
    t1.start();
    t2.start();
}

Print result:

At this time, I found that the code froze. In fact, t1 was waiting for t2 to release the lock, and t2 was also waiting for t1 to release the lock. When it comes to the knife and fork, Xiaomei can't get the knife and fork! Don't even eat steak!

The above code is written with a small probability that Zhang San will get the knife and fork at the same time, which depends on the scheduling of the CPU.

Here you can use the jconsole tool to check the thread status:

Here it is found that the thread has entered the BLOCKED state. As mentioned earlier when explaining the thread state, the BLOCKED state is a state generated while waiting for a lock. Of course, you can also add a method to obtain the thread state to the above code, and you can also find that it is a BLOCKED state:

Thread.sleep(1000); // 保证进入阻塞状态
System.out.println(t1.getState()); // 查看t1线程状态
System.out.println(t2.getState()); // 查看t2线程状态

2.3 Multiple threads and multiple locks

多个线程多把的情况跟上述 2.2 的情况差不多,相当于是 2.2 的一般情况!

在很多资料上有一个典型的案例 "哲学家就餐问题" !

有一个桌子,围着一圈哲学家,每个哲学家面前放着一碗面,哲学家两两之间放一只筷子,而桌子上的哲学家只会做两件事:吃面(获取到锁,执行后续代码)或者思考人生(阻塞等待)。

当哲学家吃面的时候,就会拿起左右手的筷子(先拿左边,再拿右边),当哲学家思考人生的时候,就会放下左右手的筷子。

如果哲学家拿到两根筷子了,就会吃面,没拿到就会思考人生!

极端情况来了!如果五个哲学家同时都拿起左手边的筷子,接着每个人都去拿自己右手边的筷子,发现右边的筷子都被别人拿走了!都要等右边的哲学家把筷子放下,此时就僵住了!由于哲学家们互不相让,此时也就形成了死锁的现象。


3. 如何避免死锁

3.1 产生死锁的四个必要条件

互斥使用:当 t1 线程拿到了锁,t2 如果也想拿,必须等着,等 t1 释放了(锁的基本特性)

不可抢占:t1 拿到了锁,必须由 t1 主动释放,t2 不能强行获取锁!

保持和请求:t1 拿到了锁 A,再拿到了 B 的锁,此时 A 这把锁还是保持的(不会因为获取到了锁 B 就把 锁 A 给释放了)

循环等待:当 t1 尝试获取锁A和B,t2也尝试获取锁B和A,如果 t1 获取到锁A,t2 获取到锁B,此时 t1 就会等待 t2 释放锁B,而 t2 也会等待 t1 释放锁 A

所以只要打破这四条的任意一条,就能让死锁消失,但前三条对于 synchronized 来说,都是基本的特性,修改不了,而循环等待上述唯一一个和代码结构相关的,也是咱们可以控制的!

所以解决死锁最简单可靠的办法,就是打破循环等待!

3.2 打破循环等待

如何打破循环等待呢?就比如上述张三和小美吃牛排的问题!

张三一把夺过刀,小美一把夺过叉,于是张三想了个公平的办法,对小美说:"这样的情况,我们都吃不到牛排,我们做个约定吧,把刀叉一起放着,数到一就一起抢,但是有一个抢的顺序,只能先刀在抢叉"。

这就好比对刀和叉进行了编号,约定好,想要拿刀叉,可以,但是必须先拿刀,后拿叉!

于是张三数到一,一瞬间张三抢到了刀,此时小美没有抢到刀,由于前面的约定,小美只能等张三放下刀了,于是张三就顺利吃到了牛排!

代码实现:

public static void main(String[] args) throws InterruptedException {
    Object knife = new Object(); //刀
    Object fork = new Object(); //叉
    Thread t1 = new Thread(() -> {
        // 先对刀加锁, 才能对叉加锁
        synchronized (knife) {
            System.out.println("张三拿到刀了!");
            synchronized (fork) {
                System.out.println("张三刀和叉都拿到了!吃牛排!");
            }
        }
    });
    Thread t2 = new Thread(() -> {
        // 先对刀加锁, 才能对叉加锁
        synchronized (knife) {
            System.out.println("小美拿到刀了!");
            synchronized (fork) {
                System.out.println("小美刀和叉都拿到了!吃牛排!");
            }
        }
    });
    t1.start();
    t2.start();
}

打印结果:

做了约定之后(对锁编号),此时死锁的问题就迎刃而解了!

那么对于哲学家吃面的问题,也是如此,对每根筷子进行编号,约定好,只能先拿左右手编号小的筷子,再拿编号大的筷子。

此时这样一来,总有一个人拿不到筷子,那么上述情况,拿到编号为 4 筷子的哲学家就能拿编号为 5 筷子吃面了,吃完了放下两支筷子,接着右手边拿到 3 筷子的哲学家就拿起放下的 编号4 筷子进行吃面了,以此类推.....

本质上我们这里讲的避免死锁的方案,就是约定加锁顺序!约定顺序后,死锁的问题就解决了!也就打破了第四点的循环等待!

其实还有一种算法,银行家算法, 但实际开发中不推荐使用,比起上述讲的办法银行家算法更复杂,也更容易出错,所以更推荐上述约定顺序的方法!

如果对银行家算法感兴趣的,可以自行查阅下相关资料!


下期预告:【多线程】volatile 关键字

Guess you like

Origin blog.csdn.net/m0_61784621/article/details/129280953