Java EE - 常见的死锁和解决方法

目录

1.可重入锁

一个线程为保证在并发过程中是线程安全的,就可以考虑加锁来规避线程安全问题,当一个线程需要频繁使用时,就需要多次加锁,如果对同一个线程多次加锁,就可能引起死锁。

例如:现在有一个线程需要完成加法函数的使用,在加锁后完成加法函数的调用;完成加法函数后还需要进行修改操作,在线程内部对修改的操作再次加锁,保证修改操作的安全。

java 复制代码
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,由于此时锁的获取需要解锁后才可申请,两个线程同时请求对方的锁,此时的两个线程就都进入阻塞等待,形成死锁。

java 复制代码
class Test1{
    //锁对象
    static Object locker1 = new Object();
    static Object locker2 = new Object();
    public static void main(String[] args) {
    //线程t1
        Thread t1 = new Thread(() -> {
            synchronized (locker1){
                //确保t2线程可以获取到locker2
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
//                    throw new RuntimeException(e);
                }
                synchronized (locker2){
                    System.out.println("t1获取到locker1和locker2");
                }
            }
        });
        //线程t2
        Thread t2 = new Thread(() -> {
            synchronized (locker2){//加锁:locker2
                synchronized (locker1){//加锁:locker1
                    System.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线程进入阻塞等待。

java 复制代码
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线程可以获取到locker2
                try {
                    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线程阻塞等待,此时还未开启线程t2
        
        t2.start();//开启线程t2
        t2.join();//main线程阻塞等待
    }
}

3.哲学家就餐

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

在极端的情况下可能每一个哲学家都先拿起左手边的一个筷子,此时每一个哲学家都可以拿到一根筷子,为了就餐,哲学家需要再拿起一根筷子,需要从右手边拿,如果此时每个哲学家都想就餐,每一个哲学家就都拿不到第二根筷子,所有哲学家就都无法就餐。

以上情形如果是在多线程的情况下执行,就会引发多个线程进行阻塞等待,可能所有线程都无法完成任务,还会消耗资源,形成死锁。

解决以上由于锁的竞争导致的死锁,可以采取按照同样的加锁顺序来获取锁。

使用上述的t1和t2线程,locker1和locker2线程的例子,线程t1和t2同时获取锁对象locker1,在获取锁对象的基础上,获取锁对象locker2。

java 复制代码
class Test1{
    //锁对象
    static Object locker1 = new Object();
    static Object locker2 = new Object();
    public static void main(String[] args){
        
        //同时获取锁对象locker1,再获取锁对象locker2
        Thread 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();//开启线程t1
        t2.start();//开启线程t2
    }
}

4.死锁形成的原因

1)锁是互斥的;一个线程获取到一把锁,其它线程申请获取到这把锁就需要阻塞等待;

2)锁是不可争夺的;线程获取到锁的资源,在解锁前,其它线程是不可享受到这把锁的资源;

3)请求和保持;线程获取到锁资源的基础上,请求获取其它线程获取的锁;

4)循环等待;多个线程申请锁的过程中,阻塞的线程间形成环。

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

相关推荐
workflower3 分钟前
软件需求规约的质量属性
java·开发语言·数据库·测试用例·需求分析·结对编程
鸣弦artha18 分钟前
Flutter框架跨平台鸿蒙开发——Build流程深度解析
开发语言·javascript·flutter
TracyCoder12322 分钟前
Java String:从内存模型到不可变设计
java·算法·string
想用offer打牌28 分钟前
Spring AI Alibaba与 Agent Scope到底选哪个?
java·人工智能·spring
情缘晓梦.32 分钟前
C++ 内存管理
开发语言·jvm·c++
黄晓琪32 分钟前
Java AQS底层原理:面试深度解析(附实战避坑)
java·开发语言·面试
我是大咖32 分钟前
二维数组与数组指针
java·数据结构·算法
姓蔡小朋友1 小时前
Java 定时器
java·开发语言
crossaspeed1 小时前
Java-SpringBoot的启动流程(八股)
java·spring boot·spring
百锦再1 小时前
python之路并不一马平川:带你踩坑Pandas
开发语言·python·pandas·pip·requests·tools·mircro