当前位置:首页 > 软件设计 > 正文内容

CountdownLatch使用不当导致的线程卡死

邻居的猫1个月前 (12-09)软件设计1371

问题现象

今天有其他项目遇到了一个问题,找了好久没有找到原因,所以找到我帮助看下。他们运用了Spring Scheduling开发了一个守时使命,然后每天早上的8点会履行一次。在DEV环境的时分是正常运转而且测试经过的,可是在发布到UAT环境之后发现了一个问题,这个守时使命只会在服务发布后触发一次,然后就再也不会调度了。

排查进程

根本查看

首要是对线上环境的装备进行了查看,首要是守时使命的装备是否有问题。

  1. 是否敞开了守时使命
@EnableScheduling
@SpringBootApplication(exclude = {
        RedisAutoConfiguration.class, RedissonAutoConfiguration.class,
        SecurityAutoConfiguration.class,
        ManagementWebSecurityAutoConfiguration.class})
public class FrontProviderApplication {
    
}

首要确定是敞开了调度使命的(实际上这个肯定是没问题的,由于在这个问题中,守时使命是触发了的,只不过只触发了一次)

  1. 调度装备是否正确,是不是装备成了一次性使命。

    @Scheduled(cron = "30 10 8 * * ?")
        public void setCollectData() throws InterruptedException {
            log.info("history=开端处理历史数据");
            historyCollectComponent.setCollectData();
            log.info("history=处理历史数据结束");
    }
    

    cron表达式

    经过查看也没有发现问题。

日志排查

装备假如没有问题的话,只能经过日志看下"案发现场"有没有什么头绪。不出预料,日志天然也是平平无奇。也没有发现有显着的反常。

日志中呈现了调度使命一开端就打印的日志,可是没有结束的日志。中心也没有发现反常。

history=开端处理历史数据

复现

排查到这儿,能够承认的是调度使命触发了,可是一向没有结束,可能是线程一向在等候。所以我预备复现下这个问题,所以把守时使命的频率调整为5分钟履行一次。这个的条件是我查看了守时使命的逻辑,也跟项目的开发搭档承认了。这个守时使命是幂等的。所以我暂时调整为5分钟一次,并不会有什么问题

调整为5分钟一次后,发现守时使命居然又正常了,5分钟一次有序进行。那这就有点伤脑筋了。这个问题暂时无法复现。

所以比较简略的方法便是,在今晚做一次服务重启,然后等候明日早上的8点,运用arthas监控一下守时使命方法的履行,看下是什么问题。

代码逻辑筛查

这么古怪的问题,要比及第二天早上才干找出原因,着实有点难过。所以我带着好奇心,耐着性质捋了下全体的代码逻辑。这个守时使命的逻辑仍是比较复杂的。可是其间中心的逻辑结构大约如下:

@Component
public class SpringCountDownLatchScheduler {

    @Scheduled(cron = "0/15 * * * * ? ")
    public void setCollectData() throws InterruptedException {
        System.out.println("history=开端处理历史数据");
        serviceA();
        System.out.println("history=处理历史数据结束");
    }


    private void serviceA() throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(2);
        serviceB(countDownLatch);
        countDownLatch.await();
    }

    private void serviceB(CountDownLatch countDownLatch) {
        new Thread() {
            @Override
            public void run() {
                System.out.println("子线程" + Thread.currentThread().getName() + "正在履行");
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("子线程" + Thread.currentThread().getName() + "履行结束");
                countDownLatch.countDown();
            }
        }.start();
        new Thread() {
            @Override
            public void run() {
                System.out.println("子线程" + Thread.currentThread().getName() + "正在履行");
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("子线程" + Thread.currentThread().getName() + "履行结束");
                countDownLatch.countDown();
            }
        }.start();
    }
}

在serviceA()方法中运用了CountDownLatch,计数器是2。然后发动两个子线程完结一系列逻辑,然后经过CountDownLatch的await()方法等候两个线程履行完,然后持续履行后续的逻辑。整个守时使命15s履行一次。

不知道咱们有没有发现这个结构的问题,我也是愣了一瞬间才发现不对劲。假如子线程在countDown()之前发生了反常呢?那主线程不就一向在等候了吗?所以我快速验证了一下:

@Component
public class SpringCountDownLatchScheduler {

    @Scheduled(cron = "0/15 * * * * ? ")
    public void setCollectData() throws InterruptedException {
        System.out.println("history=开端处理历史数据");
        serviceA();
        System.out.println("history=处理历史数据结束");
    }


    private void serviceA() throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(2);
        serviceB(countDownLatch);
        countDownLatch.await();
    }

    private void serviceB(CountDownLatch countDownLatch) {
        new Thread() {
            @Override
            public void run() {
                System.out.println("子线程" + Thread.currentThread().getName() + "正在履行");
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                if (Boolean.TRUE) {
                    throw new RuntimeException("我是反常");
                }
                System.out.println("子线程" + Thread.currentThread().getName() + "履行结束");
                countDownLatch.countDown();
            }
        }.start();
        new Thread() {
            @Override
            public void run() {
                System.out.println("子线程" + Thread.currentThread().getName() + "正在履行");
                try {
                    Thread.sleep(3000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println("子线程" + Thread.currentThread().getName() + "履行结束");
                countDownLatch.countDown();
            }
        }.start();
    }
}

在第一个线程countDown之前,自动抛一个RuntimeException来模仿第一个线程有反常的状况。

公然跟咱们预期的相同,守时使命只触发了一次之后,就卡死了。

20241124-zWe92sxawX.png

到这儿,现已找到了这个问题的根本原因,应该便是某个子线程反常了,导致主线程一向处在等候状况。那么为啥日志里看不到呢?除非代码里吞了反常,我查看了,发现并没有。

我带着这个疑问,又去日志里逐行看了一遍,发现有一行十分不起眼的Error。

2024-08-27 08:11:00.031 ERROR default 81232 [ForkJoinPool.commonPool-worker-62] o.h.engine.jdbc.spi.SqlExceptionHelper 142  142 : HikariPool-1 - Connection is not available, request timed out after 30000ms.

这下真相大白了。本来是由于数据库超时了。但仍是有个问题,为什么我调低作业频率为5分钟的时分为什么没有复现。这个我跟项目组的技能承认后发现,是由于他们早上8点左右的时分正是数据库负载最高的时分,大数据渠道会在8点左右同步数据过来。而我复现的时分是在下午,负载并不高。所以并没有呈现由于数据库超时导致守时使命反常的现象。

问题处理

知道原因之后,怎样处理就很简略了,有许多方法

  1. 优化守时使命的逻辑代码,countdown()必定要加反常处理,或许主线程await要加上超时时刻。
  2. 调整守时使命的时刻,与大数据渠道的推送峰口错开。
  3. 进步数据库资源装备。

优化后的代码结构如下:

@Component
public class SpringCountDownLatchScheduler {

    @Scheduled(cron = "0/15 * * * * ? ")
    public void setCollectData() throws InterruptedException {
        System.out.println("history=开端处理历史数据");
        serviceA();
        System.out.println("history=处理历史数据结束");
    }


    private void serviceA() throws InterruptedException {
        CountDownLatch countDownLatch = new CountDownLatch(2);
        serviceB(countDownLatch);
        countDownLatch.await();
    }

    private void serviceB(CountDownLatch countDownLatch) {
        new Thread() {
            @Override
            public void run() {
                System.out.println("子线程" + Thread.currentThread().getName() + "正在履行");
                try {
                    Thread.sleep(3000);
                    if (Boolean.TRUE) {
                        throw new RuntimeException("我是反常");
                    }
                    System.out.println("子线程" + Thread.currentThread().getName() + "履行结束");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    countDownLatch.countDown();
                }


            }
        }.start();
        new Thread() {
            @Override
            public void run() {
                System.out.println("子线程" + Thread.currentThread().getName() + "正在履行");
                try {
                    Thread.sleep(3000);
                    System.out.println("子线程" + Thread.currentThread().getName() + "履行结束");
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    countDownLatch.countDown();
                }

            }
        }.start();
    }
}

能够看到,尽管守时使命有反常,可是依然没有影响到后续的调度。

20241124-Ff7HdCRzaa.png

总结反思

这次这个问题本质上是由于CountDownLatch的运用不标准导致的。可是实际上暴露了许多隐形的问题:

  1. 项目的日志打印很不标准,错误信息欠好辨识,这也是一开端没有定位到问题的很大一个原因。针对反常信息,必定要将完好的仓库信息同时打印出来,不要只输出message。辨识度很低而且不利于排查问题原因。
  2. 最好不要手艺发动线程,而是运用线程池,而且给线程的命名供给符合实际意义的命名,这样会十分有利于排查问题。
  3. 运用CountDownLatch.countdown()时必定要注意反常处理。

[重要提示]

一切博客内容,在我的个人博客网站可见,欢迎拜访: TwoFish

扫描二维码推送至手机访问。

版权声明:本文由51Blog发布,如需转载请注明出处。

本文链接:https://www.51blog.vip/?id=303

标签: 架构理论
分享给朋友:

“CountdownLatch使用不当导致的线程卡死” 的相关文章

库存体系:应用层、范畴层、对接层的架构规划

库存体系:应用层、范畴层、对接层的架构规划

大家好,我是汤师爷~ 大厂对提名人的要求较高,即使是20k薪资的岗位,也希望应聘者可以独立承当作业责任。 关于30-40k薪资的岗位,需求具有独立体系规划和小型架构规划的才能。 技能专家和架构师岗位(30-50k以上)要求应聘者具有带领团队、担任大型体系架构的经历,并且在架构规划方面有全面且深化的理...

全网最适合入门的面向目标编程教程:11 类和目标的Python完成-子类调用父类办法-模仿串口传感器和主机

全网最适合入门的面向目标编程教程:11 类和目标的Python完成-子类调用父类办法-模仿串口传感器和主机

全网最适合入门的面向方针编程教程:11 类和方针的 Python 完结-子类调用父类办法-模仿串口传感器和主机 摘要: 本节课,咱们首要解说了在 Python 类的承继中子类怎么进行初始化、调用父类的特点和办法,一起解说了模仿串口传感器和主机类的详细完结,并运用 xcom 串口帮手与两个类进行串口通...

软件设计师证,开启软件设计职业生涯的钥匙

软件设计师证是中国计算机技术与软件专业技术资格(水平)考试(简称“软考”)中的一个中级考试。以下是关于软件设计师证考试、含金量及报名条件的详细信息: 软件设计师证考试1. 考试简介: 软件设计师考试属于全国计算机技术与软件专业技术资格考试(软考)的中级考试。通过考试的人员能够根据软件开发项目管...

面向对象数据库系统,面向对象数据库系统概述

面向对象数据库系统,面向对象数据库系统概述

面向对象数据库系统(ObjectOriented Database System,简称OODB)是一种支持面向对象编程范式的数据库管理系统。它将面向对象编程的概念(如对象、类、继承、多态等)应用于数据库系统中,使得数据库系统能够更自然地表示和处理复杂的数据结构。面向对象数据库系统的主要特点包括:1....

支付系统架构设计,支付系统架构设计概述

支付系统架构设计,支付系统架构设计概述

支付系统架构设计是一个复杂的过程,需要考虑多个方面,包括安全性、可靠性、可扩展性、易用性等。以下是一个基本的支付系统架构设计示例:1. 用户界面层(UI Layer): 用户界面层是用户与支付系统交互的界面,包括网站、移动应用、桌面应用等。 用户可以通过用户界面层进行支付操作,如输入支付...

云架构设计,构建高效、安全、可扩展的云计算环境

云架构设计是指将云计算技术应用于企业或组织的IT基础设施中,以实现更高的灵活性、可扩展性和成本效益。云架构设计通常包括以下几个关键方面:1. 需求分析:首先需要了解企业的业务需求、性能要求、安全要求等,以确定云架构的目标和范围。2. 选择云服务模型:根据企业的需求,选择合适的云服务模型,如IaaS(...