背景
种种原因,平台要添加一个有着大量定时任务,且均为类似正则解析、查询ES那种IO密集或者CPU密集型的任务的功能。一旦出现解析死循环或者查询死循环,不及时处理掉就会导致CPU飙高或者内存溢出问题。
过程趣事
有人提出,所有编写定时任务的人都在线程中sleep循环执行!
(几十个任务定时执行,加不定期添加的延时任务)我可真是栓Q!
分析需求
开发A:需要间隔一定时间抓去ES数据,例如每隔5分钟抓去最近5分钟的数据
开发B:需要延时执行一些业务处理
项目负责人:好多定时任务,能不能不要一堆定时器不要让大家都自己实现五花八门。让大家专注业务逻辑。还有就是都是很吃资源的操作,万一死循环了,别打死设备。希望能处理异常任务
总结:定时任务、延时任务、超时任务中断处理、有序顺序执行、节约线程资源
设计结构
灵感来自Reactor模型,面向事件驱动。职能分离。
实现
可能里面的有些东西大家很眼熟,不要鄙视。问就是致敬。
不废话,直接上源码
外部调用类
public class DelayTimer {
//来自netty的神秘力量,时间轮定时器。单线程,并且线程安全。
static HashedWheelTimer timer = new HashedWheelTimer();
//支持顺序执行任务的线程池
static EagerThreadPoolExecutor executor;
static {
TaskQueue<Runnable> taskQueue = new TaskQueue<Runnable>(40);
executor = new EagerThreadPoolExecutor(
4,
Runtime.getRuntime().availableProcessors(),
1,
TimeUnit.SECONDS,taskQueue,new ThreadFactory() {
@Override
public Thread newThread(Runnable r) {
return new Thread(r,"DelayTimer_"+r.hashCode());
}},new ThreadPoolExecutor.AbortPolicy());
taskQueue.setExecutor(executor);
}
//首次执行延时执行时间
private long delay;
//首次执行延时执行时间单位
private TimeUnit delayTime;
//任务:业务逻辑实现位置
private DelayTimerTask task;
private DelayTimer(){
}
public DelayTimer(DelayTimerTask timerTask, long delay, TimeUnit delayTime){
this.task = timerTask;
this.delay = delay;
this.delayTime = delayTime;
}
public void start(){
timer.newTimeout(task,delay,delayTime);
System.out.println("添加任务结束");
}
}
内部调度类
public abstract class DelayTimerTask implements TimerTask {
// 每隔几秒执行一次
private final long tick;
//超时时间
private final long timeOut;
private final TimeUnit time;
public DelayTimerTask(){
this(0,TimeUnit.MILLISECONDS,3000);
}
public DelayTimerTask(long tick, TimeUnit time, long timeOut) {
this.tick = tick;
this.time = time;
this.timeOut = timeOut;
}
@Override
public void run(Timeout timeout) {
try {
Future<?> future = DelayTimer.executor.submit(()->{
try {
doWork();
}catch (InterruptedException e){
System.out.println("IO线程中断!");
}
});
timeOutMonitor(timeout,future);
}catch (Exception e){
e.printStackTrace();
}finally {
if (tick!=0){
rePut(timeout);
}
}
}
abstract void doWork() throws InterruptedException;
private void timeOutMonitor(Timeout timeout,Future future){
timeout.timer().newTimeout((timeout1)->{
try {
future.get(0,time);
System.out.println("任务正常执行结束");
} catch (InterruptedException e) {
e.printStackTrace();
} catch (ExecutionException e) {
e.printStackTrace();
} catch (TimeoutException e) {
System.out.println("任务执行超时");
/**
*肯定有兄弟要问了,已经cancel中断了为啥还要kill线程。
*这里就涉及到知识点:对线程执行cancel操作,
*只有在线程run方法中有判断中断状态逻辑跳出执行的线程才会停止。
*cancel操作仅修改了线程的中断标志位。不会立刻停止线程
**/
future.cancel(true);
if (future.isCancelled())
killThread((FutureTask<?>) future);
e.printStackTrace();
}
},timeOut,time);
}
private void rePut(Timeout timeout){
if (timeout ==null){
return;
}
Timer timer = timeout.timer();
if (timeout.isCancelled()){
return;
}
timer.newTimeout(timeout.task(),tick, time);
}
private void killThread(FutureTask<?> submit) {
try {
// 利用反射,强行取出正在运行该任务的线程
Field runner = submit.getClass().getDeclaredField("runner");
runner.setAccessible(true);
Thread execThread = (Thread) runner.get(submit);
execThread.stop();
execThread = null;
} catch (Exception e) {
e.getMessage();
}
}
}
思考
如果线程还在队列里就超过执行时间了会怎么样?
答:线程池队列中的线程任务处于等待状态,当等待状态的线程收到中断标志时将结束任务。且因为业务与定时轮询执行的逻辑分离,不影响下一次执行该任务。
如果强制停止线程导致资源未释放问题怎么办?
答:成熟的业务兄弟该会自己回收好资源,不成熟的兄弟晚上来我家好好让你成熟一波!
为啥需求中没有谈到顺序执行,你反而加上了这个功能?
答:兄弟让我们回想一下线程池的源码。还记不记得当年大明湖畔的夏雨荷!呸!不是。
当线程池的核心线程与队列满了时,当未达到最大线程池则创建线程并将运行任务。这样以来队列里面的线程岂不是很有可能被超时。然后丢了一堆操作!所以这里使用顺序线程池。只有当线程池达到最大数时,才将任务堆放在队列中保证任务的有序执行。
补充
这里是一个简单的实现,更多细节的设计没有拿出来,本着顺便梳理一下技术点写了一点。如果兄弟们有疑问可以提出来。我好改进一下。