【小家java】记录Java守护线程使用时因忽略细节,导致的一个线上问题的排查过程(守护线程异常退出)

本文涉及的产品
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
云数据库 Tair(兼容Redis),内存型 2GB
日志服务 SLS,月写入数据量 50GB 1个月
简介: 【小家java】记录Java守护线程使用时因忽略细节,导致的一个线上问题的排查过程(守护线程异常退出)

前言


记得我在之前某一篇博文里讲到过一个案例:使用java的守护线程来模拟redis缓存的过期时间设定。


然后线程这个也是老生常谈的一个问题,守护线程也不陌生,在Jvm里就有大量的守护线程的使用。然后本文主要记录一下我在工作中使用守护线程完成业务逻辑,忽略了一点从而导致一个线上问题,进而记录排查这个过程:


基础知识:【小家java】Java里的进程、线程、协程 、Thread、守护线程、join线程的总结


业务背景


业务背景为一个供需关系模型:老师和学生


模型相当于:老师和学生都在指定一段时间里向系统签到,然后我们应用会定期的(比如20s一次)的把学生和老师撮合在一起,然后经过我们的黑匣子算法进行供需匹配,进而组成一个班级。


然后本场景的重点就在于这个每隔20s撮合一次。针对这个模型,我一下子想到的是两种方案:


  • 方案一:被动式。也就是借助调度系统,设置一个定时器被动的、盲目的每20s就去老师、学生签到队列里看一次。有就都拿出来然后经过算法组班即可。

显然,该方法有它的优势,那就是简单粗暴的处理得非常的简单,也非常容易理解。但是弊端也很明显:主动扫描我们发现绝大部分请求可能都是浪费掉了的,1%利用上的可能都不到,会消耗掉系统大量的资源。比如我们只能全天扫,20s扫一次,说实话是个非常非常大的量了。

倘若你还记录一些请求日志之类的,我相信它会对你跟踪对应系统别的功能的日志信息带来极其的不便。而且这种频率的日志输出,也给网络流量、以及磁盘IO带来了不小的负担~


  • 方案二:主动式。顾名思义,就是我有学生了主动告知你,然后你来个倒计时20s就去触发对应的组班动作即可。倘若在这20s期间有其余同学进来,那也会被一起成班嘛,这就是我们最希望的效果。


这种方式的优势:刚好就是弥补了上一种方式的不足,不用频繁的去耗费系统资源了,处理起来也更加的优雅。但是,它的实现方式就会稍微麻烦点,有两个最大的痛点吧。

第一:就是第一个人临界情况,需要考虑并发性。

第二:倒计时怎么倒?然后倒计时到了又怎么样触发对应的动作呢?

可能有人会想到,可以借助MQ中间件的延迟队列功能来做。

答:我也想过,但奈何那时候我们的技术栈还是ActiveMQ,显然是不支持的。另外说一句:即使MQ有延迟队列这样的功能,但一般MQ的官方都说了,如果是些重要的、敏感的数据,不推荐使用。


最终,我采用的是JDK的延迟队列+守护线程的方式去实现


实现方式


废话少说,show me the code(伪代码如下):

  private static final DelayQueue<RoomDelayer> CATEGORY_QUEUE = new DelayQueue<>();
    /**
     * 给处理器赋值 启动守护线程
     */
    @PostConstruct
    public void postConstruct() {
        setOperLong = redisTemplate.opsForSet();
        valueOperLong = redisTemplate.opsForValue();
        hashOperLongInt = redisTemplate.opsForHash();
        //启用一个守护线程 去监听延迟队列执行任务
        Thread t = new Thread(() -> {
            while (true) {
                RoomDelayer roomDelayer = null;
                try {
                    roomDelayer = CATEGORY_QUEUE.take();
                } catch (Exception e) {
                    log.error("take出错了", e);
                }
                if (roomDelayer == null) {
                    continue;
                }
                //获取到延迟的任务后  交给service去执行任务
                Integer configType = roomDelayer.getConfigType();
                Long courseId = roomDelayer.getCourseId();
                Long startTime = roomDelayer.getStartTime();
                Long lessonId = roomDelayer.getLessonId();
                String key = genRoomCategoryStuKey(configType, courseId, startTime, lessonId);
                try {
                    log.info("延迟队列[" + key + "]执行start...");
                    pullQueueDataPersistRoom(configType, courseId, startTime, lessonId);
                    log.info("延迟队列[" + key + "]执行end...");
                } catch (Exception e) {
                    //处理一下,万一获取锁等待超时了,导致队列丢失的问题
                    if (e instanceof RedisLockException) { //若是获取锁超时的异常 那就放进去 等待下次继续尝试
                        long execTimeAt = addEleIntoDelayQueue(configType, courseId, startTime, lessonId);
                        log.warn("因为获取执行锁超时,补贴一个延迟队列元素,信息如下: {} {} {} {} 执行时间为:{}", configType,
                                courseId, startTime, lessonId, DateUtils.date2Str(new Date(execTimeAt)));
                    } else {
                        log.info("延迟队列持久化的时候报错,key为 " + key, e);
                    }
                }
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    log.error("sleep出错了", e);
                }
            }
        });
        t.setName("daemon thread for CATEGORY_QUEUE");
        t.setDaemon(true);
        t.start();
    }


咋一看,这个应该是没有问题的。但是昨晚,我们的reids出问题了,导致我经常获取不到连接,有不少的报错。

然后到第二天,我在监控日志,发现守护线程竟然没有动静了,所以猜测是死翘翘了。


问题查找


找同事协助用jmx连上这台机器看看:如下图


image.png


再看另外一台(服务是好使的,守护线程正常):


image.png


发现这台机器的守护线程很正常的运行着。这符合我表面上看到的现象,那到底是怎么回事呢?导致线程就这么退出了?


刚开始怀疑是不是可能是内存溢出、或者内存漏洞、或者线程其它原因。所以我们尝试这看了dump日志,把日志文件down下来,本地分析。

结果为:没有找到任何名称为此的线程相关信息~


定位到原因


最后,我想。守护线程再怎么说也是个线程啊,如果执行过程中抛出异常,那就会退出线程了。然后我分析了日志,根据线程名去找该下城下的报错:


grep "daemon thread for CATEGORY_QUEUE] ERROR" /opt/g


果然找到了:


image.png


然后跟踪代码,本以为自己都给try住了,但是还是有一失误啊。


image.png


解决方案


该解决方案也是以后各位使用守护线程一定一定要注意必须做的一个方案:最外层用try包裹住,防止里面一切可能出现但又忘记了的运行时异常发生,从而终止了守护线程。那就影响非常之大了

伪代码如下:

 //启用一个守护线程 去监听延迟队列执行任务
Thread t = new Thread(() -> {
    while (true) {
          try {
             // 在这里面书写你的最核心逻辑。。。。里面抛出任何异常,我都不怕了
          } catch (Exception e) {
              log.error("daemon thread for CATEGORY_QUEUE 执行失败", e);
          }
      }
    });


总结


没什么好总结的,一句话:当你使用守护线程去处理逻辑,而必须确保此守护线程不能退出时,请无比使用try,不允许任何异常抛出~来终止守护线程即可


image.png


相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore &nbsp; &nbsp; ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库&nbsp;ECS 实例和一台目标数据库&nbsp;RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&amp;RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
5天前
|
安全 Java 调度
Java编程时多线程操作单核服务器可以不加锁吗?
Java编程时多线程操作单核服务器可以不加锁吗?
18 2
|
9天前
|
存储 缓存 Java
java线程内存模型底层实现原理
java线程内存模型底层实现原理
java线程内存模型底层实现原理
|
6天前
|
Java 编译器 索引
|
7天前
|
IDE Java 开发工具
java自定义异常20
java自定义异常20
15 3
|
7天前
|
IDE Java 开发工具
java捕获异常19
java捕获异常19
12 2
|
7天前
|
Java 调度
Java-Thread多线程的使用
这篇文章介绍了Java中Thread类多线程的创建、使用、生命周期、状态以及线程同步和死锁的概念和处理方法。
Java-Thread多线程的使用
|
4天前
|
Java 数据中心 微服务
Java高级知识:线程池隔离与信号量隔离的实战应用
在Java并发编程中,线程池隔离与信号量隔离是两种常用的资源隔离技术,它们在提高系统稳定性、防止系统过载方面发挥着重要作用。
6 0
|
7天前
|
Java 数据处理 调度
Java中的多线程编程:从基础到实践
本文深入探讨了Java中多线程编程的基本概念、实现方式及其在实际项目中的应用。首先,我们将了解什么是线程以及为何需要多线程编程。接着,文章将详细介绍如何在Java中创建和管理线程,包括继承Thread类、实现Runnable接口以及使用Executor框架等方法。此外,我们还将讨论线程同步和通信的问题,如互斥锁、信号量、条件变量等。最后,通过具体的示例展示了如何在实际项目中有效地利用多线程提高程序的性能和响应能力。
|
7天前
|
安全 算法 Java
Java中的多线程编程:从基础到高级应用
本文深入探讨了Java中的多线程编程,从最基础的概念入手,逐步引导读者了解并掌握多线程开发的核心技术。无论是初学者还是有一定经验的开发者,都能从中获益。通过实例和代码示例,本文详细讲解了线程的创建与管理、同步与锁机制、线程间通信以及高级并发工具等主题。此外,还讨论了多线程编程中常见的问题及其解决方案,帮助读者编写出高效、安全的多线程应用程序。
|
2月前
|
存储 监控 Java
Java多线程优化:提高线程池性能的技巧与实践
Java多线程优化:提高线程池性能的技巧与实践
64 1
下一篇
无影云桌面