在Java企业级开发中,Spring框架的定时任务功能(通常通过@Scheduled注解实现)因其易用性和灵活性而备受青睐。然而,当这些定时任务在生产环境中莫名停止时,往往会让开发者头疼不已。
一、常见原因剖析
1.线程或资源耗尽
- 线程池耗尽:任务中存在长时间处理操作,等同IO阻塞,新的定时任务可能会被延迟或取消
- 系统资源不足:CPU、内存等资源不足可能导致JVM响应变慢,影响定时任务的执行。
- 数据库连接池耗尽:如果定时任务依赖于数据库操作,连接池耗尽也会成为问题。
2. 代码缺陷而未捕获处理异常
- 异常处理不当:未捕获的异常可能导致任务线程异常终止,下面有原因分析。
- 静态变量/单例问题:在并发环境下,静态变量或单例的不当使用可能导致数据不一致或任务执行异常。
3. 外部依赖故障
- 网络故障:如任务中调用外部服务(如API调用),API挂掉或网络问题而未设置超时时间,等同IO阻塞。(使用HttpClient不设置超时将导致线程永久等待)
- 第三方库/框架问题:使用的第三方库或框架可能存在bug(如死循环或死锁等),等同IO阻塞。例如:一个log4j2与disruptor的bug引起的问题。
二、诊断步骤
- 检查日志:首先查看应用日志,寻找可能的错误或异常信息。在代码中插入必要的日志进行定位,找到出问题的那行代码。
- 监控工具:使用JVM监控工具或其他工具检查系统的资源:cpu、内存、线程情况。
- debug和运行环境对比:编写或运行相关的单元测试,找到问题代码。
最常见的问题是:
代码有死循环bug,代码有死锁,任务中有各种原因的异常抛出而未try-catch,各种IO阻塞,线程等资源耗尽。
一个ScheduledExecutorService启动的Java线程无故挂掉的原因是:如果使用者抛出异常,ScheduledExecutorService 将会停止线程的运行,而且不会报错,没有任何提示信息。解决方法是:try-catch
将异常信息。下面是复现代码:
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;/***一个ScheduledExecutorService启动的Java线程无故挂掉的原因是:如果使用者抛出异常,ScheduledExecutorService 将会停止线程的运行,而且不会报错,没有任何提示信息。*/
public class ScheduledExecutorServiceTest {private static int i = 0;private static int j = 10;public static void main(String[] args) {ScheduledExecutorService exc = Executors.newSingleThreadScheduledExecutor();exc.scheduleAtFixedRate(new Runnable(){@Overridepublic void run() {i++;if (i==6) {throw new RuntimeException();} else {System.out.println(i);}}}, 0, 1, TimeUnit.SECONDS);exc.scheduleWithFixedDelay(new Runnable(){@Overridepublic void run() {j++;if (j==14) {throw new RuntimeException();} else {System.out.println(j);}}}, 0, 1, TimeUnit.SECONDS);}
}
建议和处理方法是:
1. 检查是否存在IO阻塞的地方,或者代码,设置一个超时时间,如API调用,可以设置连接超时时间或读取超时时间。即同步阻塞IO换为异步非阻塞IO,或者设置超时时间。
2. fixedRate 换为 fixedDelay,后者是必须等上一次任务结束才能进行下一次任务。前者如果任务中存在长时间操作,上次任务未完成又开启一次任务,可能会导致线程池满
3. 在定时任务的最外层try-catch处理各种可能出现的异常。
作者在某项目中也遇到同样问题,在经过上面3个处理后(设置超时时间,异步非阻塞,try-catch,fixedDelay),完美解决问题。
package com.loyotech.uav.util;import com.alibaba.fastjson.JSONObject;
import com.loyotech.uav.core.config.WGlobalNames;
import lombok.extern.slf4j.Slf4j;
import okhttp3.*;import okhttp3.OkHttpClient;
import org.springframework.stereotype.Component;import java.io.File;
import java.io.IOException;
import java.util.List;
import java.util.Map;
import java.util.concurrent.TimeUnit;/*** 对okhttp的封装*/
@Component
@Slf4j
public class OkHttpManager {public String handleGet(String url){// 创建 OkHttpClient 实例OkHttpClient okHttpClient = new OkHttpClient.Builder().connectTimeout(5, TimeUnit.SECONDS).writeTimeout(10, TimeUnit.SECONDS).readTimeout(20, TimeUnit.SECONDS).build();// 构建请求Request request = new Request.Builder().url(url + WGlobalNames.safePlatformToken) // 请求的 URL.get() // 设置为 GET 请求.build(); // 构建请求对象try {// 发送同步请求Response response = okHttpClient.newCall(request).execute();// 检查请求是否成功if (response.isSuccessful()) {// 返回响应体内容return response.body().string();} else {log.error("请求失败,状态码: {},{}" , response.code(),url);}} catch (IOException e) {// 处理可能的异常log.error("请求失败,状态码: {}," ,url,e);}return null;}
}