何为死锁
死锁是多线程或并发程序中的一种情况,当多个线程因为竞争资源而相互等待,并且无法继续执行的情况。在死锁中,每个线程都在等待其他线程释放资源,从而导致所有线程都陷入无限等待状态,无法继续向前执行,最终导致程序无法完成任务。
死锁的三个典型场景
①案例一(一个线程一把锁)
如果一个线程对同一把锁,连续加了两次锁,并且该锁还是不可重入锁的时候,就会产生死锁
public synchronized void increase() {
synchronized (this) {
count++;
}
}
第一次加的锁还没有被释放,又加了一把锁,但此时该锁已经被该线程占用了,所以第二次加锁的时候,只能进行阻塞等待这样第二次的加锁在等第一次加锁的释放,同时第一次加锁又在等第二次加锁的释放,于是就形成了死锁
可重入锁和不可重入锁:
可重入锁是指同一个线程可以多次获取同一个锁,并且每次获取都要对应地释放。当一个线程已经持有锁时,再次请求该锁不会造成死锁或其他异常情况,而是允许这个线程继续获取该锁。synchronized 是可重入锁
不可重入锁是指一个线程只能获取一次锁,如果尝试再次获取同一个锁,会导致线程被阻塞。不可重入锁通常较简单,但在某些情况下可能会导致死锁。
②案例二(两个线程两把锁)
package Thread2;
import java.util.concurrent.*;
import java.util.concurrent.locks.ReentrantLock;
public class demo22 {
private static Object locker1 = new Object(); // 相当于醋
private static Object locker2 = new Object(); // 相当于辣椒
public static void main(String[] args) {
Thread t1 = new Thread(() -> { // t1线程相当于是我朋友,再有醋locker1的情况下,还想获取到我的辣椒locker2
synchronized (locker1) {
System.out.println("我目前有醋,但我还想蘸辣椒");
try {
Thread.sleep(1000);
} catch (InterruptedException e) { // 正在和我交谈,想要获取辣椒
e.printStackTrace();
}
synchronized (locker2) {
System.out.println("获取辣椒成功!等我吃完饺子就把醋和辣椒都给对方!(释放锁)");
}
}
});
t1.start();
Thread t2 = new Thread(() -> { // t2线程相当于是我,再有辣椒locker2的情况下,还想获取到我朋友的醋locker1
synchronized (locker2) {
System.out.println("我目前有辣椒,但我还想蘸醋");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (locker1) {
System.out.println("获取醋成功!等我吃完饺子就把醋和辣椒都给对方!(释放锁)");
}
}
});
t2.start();
}
}
死锁原因分析:
线程t1给对象locker1加了锁,线程t2给对象locker2加了锁;
接着线程t1想要获取对象locker2的锁,但此时locker2被线程t2占用着,t1无法获取,陷入阻塞等待(也无法释放自己占用的对象locker1的锁)几乎在同一时间,t2想要获取对象locker1的锁,但此时线程t1陷入阻塞,他所占用的locker1的锁无法正常释放。t2获取不到locker1的锁,t2无法正常工作,也无法正常释放自己占用的locker2的锁
就这样t1和t2陷入僵局,谁也无法正常释放锁,形成了死锁
解决办法
给我们的锁编号,按顺序来获取锁(规定都先蘸醋、接着蘸辣椒)
package Thread2;
import java.util.concurrent.*;
import java.util.concurrent.locks.ReentrantLock;
public class demo22 {
private static Object locker1 = new Object(); // 相当于醋
private static Object locker2 = new Object(); // 相当于辣椒
public static void main(String[] args) {
Thread t1 = new Thread(() -> { // t1线程相当于是我朋友,一开始都有醋
synchronized (locker1) {
System.out.println("我朋友说:我目前有醋,但我还想蘸辣椒");
try {
Thread.sleep(1000);
} catch (InterruptedException e) { // 正在和我交谈,想要获取辣椒
e.printStackTrace();
}
synchronized (locker2) {
System.out.println("我朋友说:获取辣椒成功!等我吃完饺子就把醋和辣椒都给对方!(释放锁)");
}
}
});
// 死锁的解决办法,多个线程在获取多个锁的时候,我们可以给这些锁编号。每个线程都按照锁的编号,从小到大的获取锁
// 一开始,我和我朋友要获取到的都是对象locker1的锁,产生竞争,竞争成功的获取到locker1的锁,失败的阻塞等待locker1锁的释放
// 竞争成功的接着又获取对象locker2的锁,此时因为另一个线程还在阻塞,没人和他竞争,直接获取locker2的锁,然后该线程结束,locker2锁、locker1锁按顺序释放
// 之前那个竞争失败的线程重写获取到locker1锁,接着又成功获取到locker2锁,最后线程结束,释放锁
t1.start();
Thread t2 = new Thread(() -> { // t2线程相当于是我,一开始都有醋
synchronized (locker1) { // 先获取编号为1的锁locker
System.out.println("我说:目前有醋,但我还想蘸辣椒");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
synchronized (locker2) {
System.out.println("我说:获取辣椒成功!等我吃完饺子就把醋和辣椒都给对方!(释放锁)");
}
}
});
t2.start();
}
}
③案例三(N个线程M把锁)
哲学家问题:
五位哲学家坐在一张圆形桌子周围的情景,每个哲学家面前有一只碗和一支筷子。哲学家们交替进行思考和就餐的活动。
问题的关键在于哲学家需要同时拿起他们自己右边和左边的筷子才能进餐,而每两个相邻的哲学家之间共享一支筷子。当多个哲学家同时想拿起相邻的筷子时,可能会出现死锁的情况。例如,如果每位哲学家都拿起他们右边的筷子,那么他们就无法继续进行。
解决办法:
为什么会出现死锁,就是因为线程对锁的互相等待,线程一要获取的锁被线程二占用着,但同时线程二要获取的锁又被线程一占用着,于是他们两个都无法获取到完整的锁,无法完成各自的进程,并释放锁。都处于一个循环等待的过程。
要解决死锁问题,重点就是解决循环等待问题。如果每个线程都按一定的顺序来获取对应的锁,比如我们给5根筷子(5把锁)按从1到5的顺序进行编号,哲学家只能拿到到左右两边锁编号最小的那把锁。这样可以避免环形等待.
死锁产生的必要条件
互斥性:当多个线程对同一把锁,有竞争。在某一时刻,最终只有一个线程可以拥有这把锁
不可抢夺性:当一个线程已经获取到了锁A,其他线程要想获取锁A,这个时候只能等该线程把A释放了之后再获取,不能中途抢夺别的线程的锁。
请求和保持性:当一个线程获取到了锁A,除非该线程自己释放锁A,否则该线程就一直保持占有锁A
循环等待性:在死锁中往往会出现,线程A等着线程B释放锁,同时线程B又在等着线程A来释放他所占有的锁,结果A、B的锁都无法正常释放,也都无法完成各自的进程,陷入了一个循环等待的状态
只要这四个条件当中有一个条件被破坏,死锁问题就可以得到解决。为了预防死锁,可以采取一些策略,如资源分配策略、资源优先级、避免占有并等待、强制抢占等.其中循环等待性这个条件最容易被破坏——我们上面的对锁进行编号,来解决死锁问题。利用的就是对循环等待性的破坏。