在我们使用多线程进行开发的时候,不可避免的会碰上死锁这类的问题,而这是我们在使用多线程中最常见的也是最棘手的问题,这篇文章就带你们初步的了解一下什么是死锁,以及如何避免死锁的产生。
一、什么是死锁
每次当别人问我什么是死锁的时候,我都会举一个每次说出来大家都觉得很好笑的例子,虽然有点老套,但真的很符合我们对死锁的定义。假设你要上班,发现没带车钥匙,车钥匙忘在家里了,但很不巧的是开门的钥匙在车里,这个时候就是悲剧的发生了~也就是触发了死锁!
通过这个例子我们可以把诱发死锁的条件用图画的形式表现出来:
首先是资源被线程给占用,导致其它线程无法使用这个资源

那就有小伙伴问了,那也还好啊,等线程一把资源用完,那线程二不就可以用了吗。这不是也避免了出现线程安全的问题吗?但线程安全问题跟死锁的本质并不是一样,线程安全是可以通过我们使用的技术进行处理,但死锁会将程序变得不能运行,是一个bug!而且是很严重的恶性bug!
还是举一个十分生动的例子,我们在日常生活中也要搬各种各样的物品,我们有时候就会陷入一种两难的境地,最经典的就是你的右手托着一叠盘子,左手托着一摞碗,等你要将盘子放在桌子上的时候,需要用左手帮你把盘子拿起来,但是你的左手却没有空余的操作空间,这个时候想必你只能傻傻的呆在原地等待其他人的帮助吧,这也是博主的亲身经历,所以经过这件事以后,我对死锁的执念非常深,因为它确确实实影响到了我的生活,这就是典型的死锁!
那这个死锁和我们上面画的“线程兄弟”有什么区别呢?答案是线程二中没有线程一想要的资源,那么线程二可以等待线程一使用完后就可以接着使用资源了,我们可以换个场景,还是这俩难兄难弟!

我们可以通过图示发现,当两个线程都想要读取其他线程占用的资源的时候,就会出现堵塞并等待,等待其他线程使用完,而其它线程也会这么想,那就会造成严重的死锁现象!所以我们可以总结出以下几点死锁的诱发条件:
1.互斥使用:当资源被一个线程占用时,其它线程不可以使用该资源。
2.不可抢占:请求资源不可以从占有者手里抢占,必须等待占有者主动释放。
3.请求和保持:资源请求者在请求其它资源的时候同时,保持对原用资源的占有。
4.循环等待:各个线程对资源占用实现一个环路结构,P1占用P2资源,P2占用P3资源,P3占用P1资源。
这些都完美符合我们上述图例的条件,所以我们的图示中“线程兄弟”就开始吵得不可开交了。
那我们该怎么帮助他们解决死锁的问题呢?
二、如何避免死锁
在上面的图示中,“线程兄弟”之所以吵架,就是因为满足了死锁触发的所有条件,那如果我们修改其中一个条件,那是不是就把死锁破坏了,也就恢复正常了。
public class DeadLock1 {
static class Banana {
public Banana() {
}
}
static class Apple {
public Apple() {
}
}
public static void main(String[] args) throws InterruptedException {
Banana banana = new Banana();
Apple apple = new Apple();
Thread threadOlderBrother = new Thread() {
@Override
public void run() {
synchronized (banana) {
System.out.println("大哥有香蕉");
try {
Thread.sleep(1000);
synchronized (apple) {
System.out.println("大哥拿到苹果");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
;
Thread threadYoungerBrother = new Thread() {
@Override
public void run() {
synchronized (apple) {
System.out.println("小弟有苹果");
try {
Thread.sleep(1000);
synchronized (banana) {
System.out.println("小弟拿到香蕉");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
};
// 线程兄弟同时开始
threadOlderBrother.start();
threadYoungerBrother.start();
// 线程兄弟同时结束
threadOlderBrother.join();
threadYoungerBrother.join();
}
}
我们用一串代码来表示我们的提到的“线程兄弟”吵架的场景,当我们执行代码的时候就会发现我们的拿到XX无法正常打印出来!

而我们的程序是没有显示异常中止的!我们可以发现引起死锁的原因是我们进行了嵌套锁定。

有时候我们要在一些场景下避免锁嵌套,我们可以跟“线程兄弟”商量说,大哥可以先用香蕉,用完香蕉直接拿出来,小弟就可以直接用香蕉了。那我们可以在原来的基础上修改我们的实现代码,我们可以把嵌套的锁拿到外面,用完就直接销毁,不会影响上锁代码块以外的代码。
public class DeadLock1 {
static class Banana {
public Banana() {
}
}
static class Apple {
public Apple() {
}
}
public static void main(String[] args) throws InterruptedException {
Banana banana = new Banana();
Apple apple = new Apple();
Thread threadOlderBrother = new Thread() {
@Override
public void run() {
synchronized (banana) {
System.out.println("大哥有香蕉");
try {
Thread.sleep(1000);
System.out.println("大哥拿出香蕉");
} catch (InterruptedException e) {
e.printStackTrace();
}
}
synchronized (apple) {
System.out.println("大哥拿到苹果");
}
}
};
;
Thread threadYoungerBrother = new Thread() {
@Override
public void run() {
synchronized (apple) {
System.out.println("小弟有苹果");
try {
Thread.sleep(1000);
System.out.println("小弟拿出苹果");
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (banana) {
System.out.println("小弟拿到香蕉");
}
}
}
};
// 线程兄弟同时开始
threadOlderBrother.start();
threadYoungerBrother.start();
// 线程兄弟同时结束
threadOlderBrother.join();
threadYoungerBrother.join();
}
}

我们可以看到”线程兄弟“终于重归于好了,我们破坏的是之前提到的死锁条件的第三点,请求和保持。
但我们要注意的是,条件一和条件二都是有关线程安全的,如果在特定场景中是不能去进行此类操作的,因为有可能会引发线程安全问题!
那么,我们可以通过条件三和条件四着手:
public class DeadLock2 {
public static void main(String[] args) throws InterruptedException {
Object lock1 = new Object();
Object lock2 = new Object();
Object lock3 = new Object();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
// t1拿了t2的锁
synchronized (lock2) {
try {
Thread.sleep(1000);
synchronized (lock1) {
System.out.println("获得t1锁");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
// t2拿了t3的锁
synchronized (lock3) {
try {
Thread.sleep(1000);
synchronized (lock2) {
System.out.println("获得t1锁");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
// t3拿了t1的锁
synchronized (lock1) {
try {
Thread.sleep(1000);
synchronized (lock3) {
System.out.println("获得t1锁");
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
});
t1.start();
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();
}
}
我们又写了一个死锁代码,这是因为循环队列导致的环路等待,我们只要把条件破坏掉就可以避免死锁的产生,我们可以将锁计时,超过时间就自动下锁,这样就避免了条件四的触发。
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
public class DeadLock2 {
public static void main(String[] args) throws InterruptedException {
Lock lock1 = new ReentrantLock();
Lock lock2 = new ReentrantLock();
Lock lock3 = new ReentrantLock();
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
// t1拿了t2的锁
try {
// 获取t2锁
if(lock2.tryLock()) {
try {
Thread.sleep(1000);
System.out.println("t1获得t2锁");
} finally {
lock2.unlock();
Thread.sleep(1000);
// 设置解锁
if(lock1.tryLock()) {
System.out.println("t1获得t1锁");
}
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
// t2拿了t3的锁
try {
// 获取t3锁
if(lock3.tryLock()) {
try {
Thread.sleep(1000);
System.out.println("t2获得t3锁");
} finally {
lock3.unlock();
Thread.sleep(1000);
// 设置解锁
if(lock2.tryLock()) {
System.out.println("t2获得t2锁");
}
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread t3 = new Thread(new Runnable() {
@Override
public void run() {
// t3拿了t1的锁
try {
// 获取t1锁
if(lock1.tryLock()) {
try {
Thread.sleep(1000);
System.out.println("t3获得t1锁");
} finally {
lock1.unlock();
Thread.sleep(1000);
// 设置解锁
if(lock3.tryLock()) {
System.out.println("t3获得t3锁");
}
}
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t1.start();
t2.start();
t3.start();
t1.join();
t2.join();
t3.join();
}
}
我们将锁设置了时间,过了1秒自动下锁,避免了占用资源导致条件三和四的产生!
如果我们两个线程中的锁对象都是一样的,那可以调整上锁的顺序避免出现死锁的情况:
public class DeadLock3 {
public static void main(String[] args) throws InterruptedException {
Object lock1 = new Object();
Object lock2 = new Object();
Thread t1 = new Thread(() -> {
synchronized (lock1) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
System.out.println("我是线程一");
}
}
});
Thread t2 = new Thread(() -> {
synchronized (lock2) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock1) {
System.out.println("我是线程二");
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
}
}
我们现在写了一个获取锁对象顺序不同的代码,运行以后就会发生死锁现象,这个时候我们要改变获取锁对象的顺序,来避免发生死锁的情况:
public class DeadLock3 {
public static void main(String[] args) throws InterruptedException {
Object lock1 = new Object();
Object lock2 = new Object();
Thread t1 = new Thread(() -> {
synchronized (lock1) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
System.out.println("我是线程一");
}
}
});
Thread t2 = new Thread(() -> {
synchronized (lock1) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (lock2) {
System.out.println("我是线程二");
}
}
});
t1.start();
t2.start();
t1.join();
t2.join();
}
}

现在我们将线程一和线程二获取锁对象的顺序调成一样的情况就可以避免发生死锁,在我们的日常生产中也有可能要使用到多线程开发的情况,那我们就得使用这些小技巧来避免发生死锁的情况。
三、总结
博主已经浅浅地介绍了死锁的相关内容,但我们在实际开发中,可能会遇到更特殊的情况,比如我们在不同类里调用锁对象,可能会导致死锁,但不能第一时间就找到问题所在,因为不知道是哪个几个类资源占用引发死锁现象。或者是在第三方依赖里面有全局的锁对象导致的死锁发生。我们都可以通过以上方法进行排查和修复,但由于死锁出现的位置往往不在一个类甚至文件中,所以需要花大量时间来修复,所以我们在设计多线程的程序时,要注重程序的健壮性,确保获取锁对象的顺序以及避免设计出全局锁对象,尽可能使用局部锁对象。这就是我对于死锁个人见解,如有出错,还希望各位指出。
534

被折叠的 条评论
为什么被折叠?



