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

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

相关推荐
Scott9999HH5 小时前
【IIoT流量实战】蒸汽管道阀门全关却仍有流量?用 Python 实现涡街信号 FFT 频谱分析与温压全补偿积算网关,深度拆解靠谱的涡街流量计厂家硬核技术标准
开发语言·python
腻害兔5 小时前
【若依项目-产品经理视角】深度拆解 RuoYi-Vue-Pro 商城模块:从商品管理到交易引擎,50 张表撑起一整套电商系统
java·大数据·vue.js·产品经理·ai编程
码智社6 小时前
AES加密原理详解及Java实现加解密实战
java·开发语言
AI云海6 小时前
python 列表、元组、集合和字典
开发语言·python
萧瑟余晖7 小时前
JDK 26 新特性详解
java·开发语言
马优晨7 小时前
Freemarker 完整讲解(后端 Java 模板引擎)
java·开发语言·freemarker·freemarker 完整讲解·freemarker模板引擎
人邮异步社区9 小时前
怎么把C语言学到精通?
c语言·开发语言
心平气和量大福大10 小时前
C#-WPF-控件-TextBox 数据绑定
开发语言·c#·wpf
ttwuai10 小时前
Cursor 生成 CRUD 后,Go 后台接口别只测 200:JWT、RBAC 和 tenant_id 怎么验
开发语言·后端·golang
维天说10 小时前
CLI-Switch 2026年3月版历史设计:Hook、TTY 隔离与 JSON 状态
java·服务器·json