一、需求描述
工作中遇到一个任务,对定时任务里开启的线程池中执行的异步任务进行错误日志监控,保证定时任务能正常运行,且出先异常时,运维人员能快速通过界面找到出现问题的任务。
下面是定时任务的部分伪代码(省略具体业务):
class Test2 {
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(5,
10,
60L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(200));
@Scheduled(cron = "0 0 6 * * ?")
public void testScheuled() {
//模拟业务:获取一个List
List<String> strArr = new ArrayList<>();
for (String str : strArr) {
//启动异步任务
threadPool.execute(() -> {
//处理主要业务
// ...
});
}
}
//.....省略其他定时任务.....
}
上述代码,主要列出了逻辑流程,具体业务代码省略,同样这个类的定时任务也有很多,这里只列出一个,便于演示。
代码解析:一个定时任务,有一个for循环,循环多次向线程池提交异步任务。
需求
逻辑:这里我们要做的事情就是对for循环里所提交的任务进行监控,若任务执行过程中出错了就需要记录下该异常,将异常信息记录(一个定时任务只需记录其中一个出错的线程异常即可),并且将该定时任务执行状态改为失败;若所有任务都执行成功了,那就无需记录任何信息,但要将该定时任务的执行状态改为成功。
运维页面:需要展示的信息有定时任务名称、定时任务code(唯一标识)、错误原因(可为空)、执行状态(成功或失败)、收集时间
二、代码实现(不考虑代码的美观)
针对上述需求,我们初步做了功能实现,实现起来也很简单:
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(5,
10,
60L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(200));
@Scheduled(cron = "0 0 6 * * ?")
public void testScheuled() {
Map<String, String> errMap = new ConcurrentHashMap<>(2);
//模拟业务:获取一个List
List<String> strArr = new ArrayList<>();
for (String str : strArr) {
//启动异步任务
threadPool.execute(() -> {
try {
//处理主要业务
// ...
} catch (Exception e) {
if (errMap.get(ERRMSG) == null) {
errMap.put(ERRMSG, ERRMSG);
//设置监控结果
setCheckResult("自检码", "监控结果为False", e.getMessage(), 1, 1);
}
} finally {
if (errMap.get(ERRMSG) == null && errMap.get(SUCCESS) == null) {
errMap.put(SUCCESS, SUCCESS);
//设置监控结果
setCheckResult("自检码", "监控结果True", "", 1, 1);
}
}
});
}
}
private void setCheckResult(String checkCode, String checkResult, String checkErr, int dateValue, int field) {
//....
//写入数据库
//.....
}
代码解释:对比原代码和新代码的变化,显然就是在线程的run方法内加了try…catch…finally…并在其中做了相关的错误信息记录,具体的错误信息处理已经封装成一个方法了,这里不重点介绍其中实现,也就是记录数据库,再查寻展示于界面。(错误信息处理,根据个人需求,不同场景需求下,实现方式也不一样,因此不做过多介绍)
Map<String, String> errMap = new ConcurrentHashMap<>(2);//用于判断是否出现过异常的标识
简单介绍一下,为什么要用map,而不是用一个局部变量做错误标志?
不用普通变量,因为使用了lambada表达式,所以是无法访问到的。
ConcurrentHashMap,可以被lambada表达式内所访问,且线程安全。
…
具体为何Map可以被访问而普通变量却不能访问,本人也还没有时间深究。
至此,代码写完了,功能也完美实现了,也完全预防了以下问题:
- 全部线程都出错时,出现多次向数据库写入错误信息的情况,通过map控制后,捕捉到一个线程出错后就仅写入一次,其他忽略。
- 全部成功时,出现多次向数据库写入成功信息的情况,同样的通过map控制后,也只写入了一次。
- 某次执行出现失败并记录了数据库为F,修复后又如何更新数据库为S?要知道,定时任务每次重新执行,map都是重新创建的,因此这个问题可以忽略。
任务完成了,高高兴兴去交任务,没想到被打回来了,理由:实现代码繁琐,代码侵略性大,重复代码多,项目定时任务较多,每个定时任务加一遍,累死个人。
三、代码优化(装饰器模式+模板方法模式)
看着屏幕上的那么多重复代码,被打回来也是正常,我还是太菜了…hahahahahah
参考了几个设计模式,我又对我的代码进行了下面的改正:
核心业务代码如下:
ThreadPoolExecutor threadPool = new ThreadPoolExecutor(5,
10,
60L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(200));
@Scheduled(cron = "0 0 6 * * ?")
public void testScheuled() {
Map<String, String> errMap = new ConcurrentHashMap<>(2);
//模拟业务:获取一个List
List<String> strArr = new ArrayList<>();
for (String str : strArr) {
//启动异步任务
threadPool.execute((new ExecutorTask("自检码", "监控结果True", "", 1, 1,errMap){
@Override
public void task() {
//处理主要业务
// ...
}
}));
}
}
非核心业务代码:
abstract class ExecutorTask implements Runnable{
private String code;
private int dateValue;
private int field;
private Map<String, String> errMap;
public ExecutorTask(String code,int dateValue,int field,Map<String, String> errMap){
this.code = code;
this.dateValue = dateValue;
this.field = field;
this.errMap = errMap;
}
public abstract void task();
public void run(){
try {
task();
} catch (Exception e) {
if (errMap.get(ERRMSG) == null) {
errMap.put(ERRMSG, ERRMSG);
setCheckResult(code, EnumType.CheckResult.F, e.getMessage(), dateValue, field);
}
} finally {
if (errMap.get(ERRMSG) == null && errMap.get(SUCCESS) == null) {
errMap.put(SUCCESS, SUCCESS);
setCheckResult(code, EnumType.CheckResult.S, SPACE_STRING, dateValue, field);
}
}
}
private void setCheckResult(String checkCode, String checkResult, String checkErr, int dateValue, int field) {
//....
//写入数据库
//.....
}
}
经过一顿操作,成功的把非核心业务代码提取处理,对原定时任务的代码入侵减少了,实现起来没那么繁琐了,再加入定时任务时,也只需要重点关注核心逻辑即可。显然成功抽离了大部分非核心代码。
简单解释一下ExecutorTask 类
ExecutorTask 实现了runable接口,并对其中的run方法做了实现,而run方法中对task方法进行catch异常操作,然后将task交由子类实现,而实现的代码便是咱们的核心业务代码。
这其中借鉴了23种设计模式种的几种设计模式,本人也有些分不太清,感觉和适配器模式、装饰器模式、模板方法模式都挺像的,这些大家都可以去了解下。
至此,本人又兴高采烈的去交任务了,然而又打回来了,理由如下:
- 核心代码里的这行Map<String, String> errMap = new
ConcurrentHashMap<>(2);别人看了会怎么想?这是什么东西,为啥要出现在我的代码里? - 还有这行 threadPool.execute((new ExecutorTask(“自检码”, “监控结果True”, “”, 1,
1,errMap),凭啥保证别人会乖乖给你new一个ExecutorTask,又乖乖的传入后面几个鬼参数,而且现在都流行lambada表达式,这个内部类又是什么鬼?
这些问题,我也想过,本以为可以蒙混过去,也是想试探一下组长的态度,看来是我想多了…hahahahahha…针对这几个问题,我回到座位又陷入了漫长漫长的沉思…
四、代码再优化(装饰器模式+模板方法模式+适配器)
看着这几个问题,确实都是个问题,想了一下,也许可以对线程池做一下封装,于是又得到了下面的代码:
核心业务代码:
MyThreadPoolExecutor threadPool = new MyThreadPoolExecutor(5,
10,
60L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(200));
@Scheduled(cron = "0 0 6 * * ?")
public void testScheuled() {
//开启监控,这三个参数是记录错误信息需要的,根据不同的定时任务,传入相应的值
threadPool.beginMonitor("自检码",1, 1);
//模拟业务:获取一个List
List<String> strArr = new ArrayList<>();
for (String str : strArr) {
//启动异步任务
threadPool.execute(()->{
//...
//核心业务逻辑
//...
});
}
}
非核心业务代码:
class MyThreadPoolExecutor{
private ThreadPoolExecutor poolExecutor;
private String code;
private int dateValue;
private int unit;
private boolean monitor = false;
private Map<String, String> errMap;
public MyThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this.poolExecutor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,Executors.defaultThreadFactory());
}
public void beginMonitor(String code, int dateValue, int unit){
this.code = code;
this.dateValue = dateValue;
this.unit = unit;
this.monitor = true;
this.errMap = new ConcurrentHashMap<>(2);
}
public void execute(Runnable run){
if (monitor){
ExecutorTask executorTask = new ExecutorTask(code,dateValue,unit,errMap,run);
poolExecutor.execute(executorTask);
}else {
poolExecutor.execute(run);
}
}
}
class ExecutorTask implements Runnable {
private String code;
private int dateValue;
private int unit;
private Runnable task;
private Map<String, String> errMap;
public ExecutorTask(String code, int dateValue, int unit, Map<String, String> errMap,Runnable task) {
this.code = code;
this.dateValue = dateValue;
this.unit = unit;
this.task = task;
this.errMap = errMap;
}
public void run() {
try {
task.run();
} catch (Exception e) {
if (errMap.get(ERRMSG) == null) {
errMap.put(ERRMSG, ERRMSG);
setCheckResult(code, EnumType.CheckResult.F, e.getMessage(), dateValue, unit);
}
} finally {
if (errMap.get(ERRMSG) == null && errMap.get(SUCCESS) == null) {
errMap.put(SUCCESS, SUCCESS);
setCheckResult(code, EnumType.CheckResult.S, SPACE_STRING, dateValue, unit);
}
}
}
private void setCheckResult(String checkCode, String checkResult, String checkErr, int dateValue, int field) {
//....
//写入数据库
//.....
}
}
由上面的代码可以看到,主要核心的业务代码已经清净了不少,与原代码相比,几乎没什么区别,已经是完美优化了。
与原代码的主要区别:对线程池做了一层封装,并通过对execute的功能做了增强。
看着这份代码,心想着,改了一遍又一遍,推翻推翻再推翻,终于干净,于是我又想赶快把这份代码交上去,看他怎么说…
刚要挪开脚步,突然想起前两次都被推翻,这次要谨慎点,再仔细检查一遍看看有啥bug没~~~~~~~
果不其然,这次我自己发现了问题,唉~~~
在自定义的线程池里,那几个成员变量是公共资源,每个定时任务就是一条线程,每个都会调用一次beginMonitor方法,这就导致了那几个成员变量出现线程安全问题。
难道无解了吗?如何保证那几个成员变量的线程安全呢?
五、ThreadLocal解决线程安全问题
晚上,心思重重的我再次打开电脑,盯着这份代码思考,偶尔百度下~~~无解。
关电脑睡觉,躺床上依旧思考…难道又回归到原点了吗?不对,等等等等…ThreadLocal…
立马爬起来,才凌晨而已,还挺早哦…不急着睡,经过三两下捣腾,搞定。
核心业务代码不变:
MyThreadPoolExecutor threadPool = new MyThreadPoolExecutor(5,
10,
60L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(200));
@Scheduled(cron = "0 0 6 * * ?")
public void testScheuled() {
//开启监控
threadPool.beginMonitor("自检码",1, 1);
//模拟业务:获取一个List
List<String> strArr = new ArrayList<>();
for (String str : strArr) {
//启动异步任务
threadPool.execute(()->{
//...
//核心业务逻辑
//...
});
}
}
对线程池的成员变量改为ThreadLocal线程变量:
class MyThreadPoolExecutor{
private ThreadPoolExecutor poolExecutor;
ThreadLocal<MonitorInfo> monitorInfo = new ThreadLocal();;
ThreadLocal<Map> errMap = new ThreadLocal();
public MyThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this.poolExecutor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue,Executors.defaultThreadFactory());
}
public void beginMonitor(MonitorInfo info){
monitorInfo.set(info);
errMap.set(new ConcurrentHashMap<>(2));
}
public void execute(Runnable run){
if (monitorInfo.get() != null){
ExecutorTask executorTask = new ExecutorTask(monitorInfo.get(),errMap.get(),run);
poolExecutor.execute(executorTask);
}else {
poolExecutor.execute(run);
}
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
class MonitorInfo{
private String code;
private int dateValue;
private int unit;
}
class ExecutorTask implements Runnable {
private MonitorInfo info;
private Runnable task;
private Map<String, String> errMap;
public ExecutorTask(MonitorInfo info, Map<String, String> errMap,Runnable task) {
this.info = info;
this.task = task;
this.errMap = errMap;
}
public void run() {
try {
task.run();
} catch (Exception e) {
if (errMap.get(ERRMSG) == null) {
errMap.put(ERRMSG, ERRMSG);
setCheckResult(EnumType.CheckResult.F, e.getMessage(), info);
}
} finally {
if (errMap.get(ERRMSG) == null && errMap.get(SUCCESS) == null) {
errMap.put(SUCCESS, SUCCESS);
setCheckResult(EnumType.CheckResult.S, SPACE_STRING, info);
}
}
}
private void setCheckResult(String checkResult, String checkErr,MonitorInfo info) {
//....
//写入数据库
//.....
}
}
ThreadLocal为线程变量,是线程安全的,每个定时任务运行时,都是一个线程,因此存入于ThreadLocal内的值也都是线程安全的,避免了互相干扰。
(但是,再我以为问题解决了后,突然想到了一个严重的问题,每个子任务都是一个新的线程,所以子任务线程,是无法共享monitorInfo 里面值的。看样子这个问题暂时无解了)
这个问题暂时放下了,时光匆匆,我离职了,又入职了,又离职了,又入职了…
·····
····
··
六、一年后
两年后的今天,翻到了这个遗留问题,博客也一直是私密的,想着再回顾一下当时的场景,看以我如今的水平能不能解决,抱着尝试的心态就干了起来。
·······
····
···
··
nice!!解决了。
定时任务:
@Component
public class ScheduledTask4 {
MyThreadPoolExecutor threadPool = new MyThreadPoolExecutor(6,
10,
60L,
TimeUnit.SECONDS,
new ArrayBlockingQueue<Runnable>(200));
@Scheduled(fixedDelay= 10000)
public void scheduledTask2() throws InterruptedException {
MonitorInfo monitor = threadPool.getMonitor("自检码1", 1, 1);
for (int i = 0;i<10;i++) {
threadPool.execute(monitor,() -> {
// System.out.println(Thread.currentThread().getName()+"**********自检码1********"+ LocalDateTime.now());
});
}
}
@Scheduled(fixedDelay= 10000)
public void scheduledTask3() throws InterruptedException {
MonitorInfo monitor = threadPool.getMonitor("自检码2", 1, 1);
for (int i = 0;i<10;i++) {
threadPool.execute(monitor,() -> {
// System.out.println(Thread.currentThread().getName()+"**********自检码2********"+ LocalDateTime.now());
});
}
}
@Scheduled(fixedDelay= 10000)
public void scheduledTask4() throws InterruptedException {
MonitorInfo monitor = threadPool.getMonitor("自检码3", 1, 1);
for (int i = 0;i<10;i++) {
threadPool.execute(monitor,() -> {
// System.out.println(Thread.currentThread().getName()+"**********自检码2********"+ LocalDateTime.now());
int j = 10/0;
});
}
}
}
自定义线程池:
class MyThreadPoolExecutor{
private ThreadPoolExecutor poolExecutor;
public MyThreadPoolExecutor(int corePoolSize,
int maximumPoolSize,
long keepAliveTime,
TimeUnit unit,
BlockingQueue<Runnable> workQueue) {
this.poolExecutor = new ThreadPoolExecutor(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, Executors.defaultThreadFactory());
}
public MonitorInfo getMonitor(String name, int a, int b){
return new MonitorInfo(name, a, b,new ConcurrentHashMap<>());
}
public void execute(MonitorInfo monitor,Runnable run){
if (monitor != null){
ExecutorTask executorTask = new ExecutorTask(monitor,run);
poolExecutor.execute(executorTask);
}else {
poolExecutor.execute(run);
}
}
}
@Data
@AllArgsConstructor
@NoArgsConstructor
class MonitorInfo{
private String code;
private int dateValue;
private int unit;
private Map<String,String> errMap;
}
class ExecutorTask implements Runnable {
private Runnable task;
private MonitorInfo monitor;
public ExecutorTask(MonitorInfo monitor,Runnable task) {
this.task = task;
this.monitor = monitor;
}
public void run() {
Map<String, String> errMap = null;
try {
errMap = monitor.getErrMap();
task.run();
System.out.println("线程"+Thread.currentThread().getName()+"处理了"+monitor.getCode());
} catch (Exception e) {
if (errMap.get(ERRMSG) == null) {
errMap.put(ERRMSG, ERRMSG);
// setCheckResult(EnumType.CheckResult.F, e.getMessage(), info);
System.out.println("线程"+Thread.currentThread().getName()+"处理了"+monitor.getCode()+"出现异常。");
}
} finally {
if (errMap.get(ERRMSG) == null && errMap.get(SUCCESS) == null) {
errMap.put(SUCCESS, SUCCESS);
// setCheckResult(EnumType.CheckResult.S, SPACE_STRING, info);
}
}
}
private void setCheckResult(String checkResult, String checkErr,MonitorInfo info) {
//....
//写入数据库
//.....
}
}