Java EE - 常见的死锁和解决方法
目录
- 1.可重入锁
- 2.两个线程两把锁
- 3.哲学家就餐
- 4.死锁形成的原因
1.可重入锁
一个线程为保证在并发过程中是线程安全的,就可以考虑加锁来规避线程安全问题,当一个线程需要频繁使用时,就需要多次加锁,如果对同一个线程多次加锁,就可能引起死锁。
例如:现在有一个线程需要完成加法函数的使用,在加锁后完成加法函数的调用;完成加法函数后还需要进行修改操作,在线程内部对修改的操作再次加锁,保证修改操作的安全。
class Test{//锁对象static Object locker = new Object();//求和static int sum;public static void main(String[] args) throws InterruptedException {//定义两个操作数int a = 1;int b = 2;//创建一个线程Thread thread = new Thread(() -> {synchronized (locker){//第一次加锁sum = a + b;//计算加法//此时修改sum变量synchronized (locker){//第二次加锁sum -= (a + b);}}});//开启线程thread.start();thread.join();System.out.println("sum = " + sum);}
}
线程thread在进行加法操作时就第一次加锁,锁对象是定义的locker,第二次修改操作加锁的对象也是locker,同一线程使用同一个锁对象加了两次锁,如果按照锁的性质,锁是互斥的,一个线程获取到锁时,另一个线程需要获取到这个锁就需要进入阻塞等待,所以程序运行后线程thread应该会进入阻塞等待。
执行程序的结果:正常输出sum = 0;

在程序运行后可以正常的输出结果,线程thread并没有进入阻塞等待。
为什么对同一个线程重复加锁不会形成死锁呢?
在Java中引入了可重入锁,对于同一个线程使用同一个锁对象多次加锁,并不会真正形成死锁,而是在加锁前进行检查,发现锁对象已经对线程加锁,在程序运行前会优化为不加锁,即加锁操作只针对第一次,后续加锁操作会被当作不加锁。
如何设计一个可重入锁呢?
1)记录第一次加锁的对象和线程;
2)每一次加锁前都进行锁对象和线程加锁情况的检查。
2.两个线程两把锁
创建两个线程t1和t2,定义两个锁对象locker1和locker2,两个线程并发执行,t1线程先获取锁对象locker1,t2线程获取锁对象locker2,在保持获取锁的基础上,线程t1请求获取锁对象locker2,线程t2请求获取锁对象locker1,由于此时锁的获取需要解锁后才可申请,两个线程同时请求对方的锁,此时的两个线程就都进入阻塞等待,形成死锁。

class Test1{//锁对象static Object locker1 = new Object();static Object locker2 = new Object();public static void main(String[] args) {//线程t1Thread t1 = new Thread(() -> {synchronized (locker1){//确保t2线程可以获取到locker2try {Thread.sleep(1);} catch (InterruptedException e) {
// throw new RuntimeException(e);}synchronized (locker2){System.out.println("t1获取到locker1和locker2");}}});//线程t2Thread t2 = new Thread(() -> {synchronized (locker2){//加锁:locker2synchronized (locker1){//加锁:locker1System.out.println("t2获取到locker1和locker2");}}});//开启线程t1.start();t2.start();}
}
以上程序并发执行会导致t1线程阻塞等待获取锁对象locker2,t2线程阻塞等待获取锁对象locker1,形成死锁,在默认路劲C:\Program Files\Java\jdk-17\bin找到jconsole打开,发现t1(Thread-0)线程被t2(Thread-1)线程阻塞,t2(Thread-1)线程被t1(Thread-0)线程阻塞。


需要解决此类死锁可以将线程改为串行执行,程序就可以正常运行,先开启线程t1,再开启线程t2,开启后调用join方法使main线程进入阻塞等待。
class Test1{//锁对象static Object locker1 = new Object();static Object locker2 = new Object();public static void main(String[] args) throws InterruptedException {Thread t1 = new Thread(() -> {synchronized (locker1){//确保t2线程可以获取到locker2try {Thread.sleep(1);} catch (InterruptedException e) {
// throw new RuntimeException(e);}synchronized (locker2){System.out.println("t1获取到locker1和locker2");}}});Thread t2 = new Thread(() -> {synchronized (locker2){synchronized (locker1){System.out.println("t2获取到locker1和locker2");}}});t1.start();t1.join();//main线程阻塞等待,此时还未开启线程t2t2.start();//开启线程t2t2.join();//main线程阻塞等待}
}

3.哲学家就餐
现在有n个哲学家围在同一桌就餐,但是在餐桌上只提供n根筷子,筷子放在哲学家之间,哲学家需要就餐需要两根筷子,但是左右的筷子除了本人能拿到,相邻的哲学家也可拿到,如何解决哲学加就餐问题?

在极端的情况下可能每一个哲学家都先拿起左手边的一个筷子,此时每一个哲学家都可以拿到一根筷子,为了就餐,哲学家需要再拿起一根筷子,需要从右手边拿,如果此时每个哲学家都想就餐,每一个哲学家就都拿不到第二根筷子,所有哲学家就都无法就餐。
以上情形如果是在多线程的情况下执行,就会引发多个线程进行阻塞等待,可能所有线程都无法完成任务,还会消耗资源,形成死锁。
解决以上由于锁的竞争导致的死锁,可以采取按照同样的加锁顺序来获取锁。
使用上述的t1和t2线程,locker1和locker2线程的例子,线程t1和t2同时获取锁对象locker1,在获取锁对象的基础上,获取锁对象locker2。
class Test1{//锁对象static Object locker1 = new Object();static Object locker2 = new Object();public static void main(String[] args){//同时获取锁对象locker1,再获取锁对象locker2Thread t1 = new Thread(() -> {synchronized (locker1){synchronized (locker2){System.out.println("t1获取到locker1和locker2");}}});Thread t2 = new Thread(() -> {synchronized (locker1){synchronized (locker2){System.out.println("t2获取到locker1和locker2");}}});t1.start();//开启线程t1t2.start();//开启线程t2}
}

4.死锁形成的原因
1)锁是互斥的;一个线程获取到一把锁,其它线程申请获取到这把锁就需要阻塞等待;
2)锁是不可争夺的;线程获取到锁的资源,在解锁前,其它线程是不可享受到这把锁的资源;
3)请求和保持;线程获取到锁资源的基础上,请求获取其它线程获取的锁;
4)循环等待;多个线程申请锁的过程中,阻塞的线程间形成环。

以上就是本篇文章的所有内容,如果有任何疑问,欢迎评论区讨论留言,我们下一篇文章再见!
