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)循环等待;多个线程申请锁的过程中,阻塞的线程间形成环。

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

相关推荐
wangmengxxw2 小时前
Swagger技术
java·swagger
全干engineer3 小时前
idea拉取github代码 -TLS connect error 异常问题
java·github·intellij-idea
10岁的博客3 小时前
二维差分算法高效解靶场问题
java·服务器·算法
百***93503 小时前
Tomcat报404问题解决方案大全(包括tomcat可以正常运行但是报404)
java·tomcat
qq_281317473 小时前
kubernetes(k8s)-pod生命周期
java·容器·kubernetes
IT界的奇葩3 小时前
代码规范 spring-javaformat使用
java·spring·代码规范
披着羊皮不是狼3 小时前
多用户跨学科交流系统(4)参数校验+分页搜索全流程的实现
java·spring boot
AI_56784 小时前
接口测试“零基础通关“:Postman从入门到自动化测试实战指南
开发语言·lua
是Yu欸4 小时前
Rust 并发实战:从零构建一个内存安全的“番茄时钟”
开发语言·安全·rust