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

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

相关推荐
天天摸鱼的小学生9 小时前
【Java Enum枚举】
java·开发语言
阿猿收手吧!9 小时前
【C++】cpp虚函数和纯虚函数的声明和定义
开发语言·c++
q_30238195569 小时前
Python实现基于多模态知识图谱的中医智能辅助诊疗系统:迈向智慧中医的新篇章
开发语言·python·知识图谱
爬山算法9 小时前
Redis(168) 如何使用Redis实现会话管理?
java·数据库·redis
程语有云9 小时前
生产事故-那些年遇到过的OOM
java·内存·oom·生产事故
雨中飘荡的记忆9 小时前
Spring Test详解
java·后端·spring
梨落秋霜9 小时前
Python入门篇【输入input】
开发语言·python
wen-pan9 小时前
Go 语言 GMP 调度模型深度解析
开发语言·go
Buxxxxxx9 小时前
DAY 34 模块和库的导入
开发语言·python
sugar__salt9 小时前
网络编程套接字(二)——TCP
java·网络·网络协议·tcp/ip·java-ee·javaee