并发编程之定时线程池&定时任务
1、定时线程池
1.1、定时线程池的由来
ScheduledThreadPoolExecutor定时线程池是继承自 ThreadPoolExecutor 类和实现了 ScheduledExecutorService类。
如下图所示:
1.2、ScheduledThreadPoolExecutor的api
1.2.1、schedule
schedule api是将任务延迟执行,如下面代码,run方法里面的代码将会在线程启动后5s后执行,代码如下:
private static Logger logger = Logger.getLogger(Log.class.getName());
public static void main(String[] args) {
ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(1);
logger.info("开始的时候!");
scheduledThreadPoolExecutor.schedule(new Runnable() {
@Override
public void run() {
logger.info("我要延迟5s执行!");
}
},5000, TimeUnit.MILLISECONDS);
}
程序执行结果截图:
17s = => 22s
1.2.2、scheduleAtFixedRate
它有两个参数,
第一个参数是线程再启动后延迟多长执行
第二个参数是循环执行的周期是多长时间
下面这个就是在线程启动后延迟一秒,然后以两秒的周期循环执行
private static Logger logger = Logger.getLogger(Log.class.getName());
public static void main(String[] args) {
ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(1);
logger.info("开始的时候!");
scheduledThreadPoolExecutor.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
logger.info("执行。。。。");
}
},1000,2000,TimeUnit.MILLISECONDS);
}
执行结果去下图所示:
问题:如果任务执行的时间大于间隔周期的时间怎么办?
任务会在阻塞队列里面排队,当上一个任务执行完成之后刚空出来就会去执行下一个任务,代码如下所示:
private static Logger logger = Logger.getLogger(Log.class.getName());
public static void main(String[] args) {
ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(1);
logger.info("开始的时候!");
scheduledThreadPoolExecutor.scheduleAtFixedRate(new Runnable() {
@Override
public void run() {
logger.info("执行。。。。");
long start = System.currentTimeMillis();
while(true){
long end = System.currentTimeMillis();
if(end - start > 5000){
break;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
},1000,2000,TimeUnit.MILLISECONDS);
}
执行结果如下图所示:
注意:这里虽然是5秒执行一个任务,但是在阻塞队列中一定排了很多任务,即使我们将线程池扩大,也无济于事,因为我们一个任务只能由一个线程执行。
1.2.3、scheduleWithFixedDelay
由于在很多时候我们不知道一个任务到底能执行多长时间,所以就有了下面这个api,就是当我们上一个任务执行完成之后,再间隔一段时间去进行下一次循环,代码如下所示:
private static Logger logger = Logger.getLogger(Log.class.getName());
public static void main(String[] args) {
ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(1);
logger.info("开始的时候!");
scheduledThreadPoolExecutor.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
logger.info("执行。。。。");
long start = System.currentTimeMillis();
while(true){
long end = System.currentTimeMillis();
if(end - start > 5000){
break;
}
try {
Thread.sleep(100);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
},1000,2000,TimeUnit.MILLISECONDS);
}
执行截图如下所示:
任务基本7s执行一次
1.3、定时线程池应用场景
1、Redis的分布式锁
如果我们的一个Redis有很多的客户端,当一个客户端拿到一个锁之后,突然挂掉了,这个时候我们这个锁资源会有一个过期时间,正常情况下是可以满足的,但是如果说我们这块的业务逻辑比较复杂的话,这个过期时间还不够,那么就会存在别的线程获取到当前锁资源的情况,所以这个时候就可以用到我们的这个定时线程池了,我们这个定时线程池在隔一段时间检测一下这个锁,如果还存在就给他续上几秒,直至这个锁被释放,这样,就无论你这个任务执行多久,都会保证把这个任务执行完之后,锁才会释放。
2、zk注册中心与服务发现
当我们的项目是微服务架构的话,我们每个系统只用去访问zk,zk会根据我们上送的服务名去自动分配活着的相应服务,而这些活着的服务信息是怎么来的呢,就是他们会每隔一段时间将自己的配置信息等情况发送给zk,zk拿到之后做管理,后续再相应其它服务的分配。这种隔一段时间就上送自己信息的方式就可以用ScheduledThreadPoolExecutor定时线程池做。
3、定期刷新缓存等等。
2、定时线程池源码简析
下面我以scheduleAtFixedRate api举例,其它的都差不多
首先我们进入到这个方法
2.1、scheduleAtFixedRate
scheduleAtFixedRate的构造方法
1、对任务进行排序。
2、将任务存储起来。
3、走延时执行方法。
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,
long initialDelay,
long period,
TimeUnit unit) {
if (command == null || unit == null)
throw new NullPointerException();
if (period <= 0)
throw new IllegalArgumentException();
// 通过内部的一个包装,实现任务的排序,排序的方式是堆
ScheduledFutureTask<Void> sft =
new ScheduledFutureTask<Void>(command,
null,
triggerTime(initialDelay, unit),
unit.toNanos(period));
// 里面什么都没干,留给拓展用
RunnableScheduledFuture<Void> t = decorateTask(command, sft);
// 将当前任务存储在 outerTask 成员变量中
sft.outerTask = t;
// 执行延时执行
delayedExecute(t);
return t;
}
2.2、delayedExecute
延时执行方法
1、如果线程池已挂掉,执行拒绝策略。
2、将任务放进队列中,再次判断线程池的状态,如果没问题,则找线程执行。
private void delayedExecute(RunnableScheduledFuture<?> task) {
// 如果线程池已挂掉,执行拒绝策略
if (isShutdown())
reject(task);
else {
// 否则,将任务直接放在队列中
super.getQueue().add(task);
// 再次判断线程池的状态是否有问题
if (isShutdown() &&
!canRunInCurrentRunState(task.isPeriodic()) &&
remove(task))
task.cancel(false);
else
// 若没有问题,创建线程,来执行任务
ensurePrestart();
}
}
2.3、ensurePrestart
主要就是创建线程去执行任务
void ensurePrestart() {
// 取出线程池中的线程数量
int wc = workerCountOf(ctl.get());
// 如果小于核心线程数,创建一个核心线程来执行任务
if (wc < corePoolSize)
addWorker(null, true);
// 工作线程都等于0了还没有核心线程数大,那核心线程数的配置也只能是0了
// 能走到这块说明核心线程数参数为0,但是任务还是要执行,所以启动一个非核心线程
else if (wc == 0)
addWorker(null, false);
// 表示核心线程已经创建满,则等待核心线程腾出来再执行,因为前面已经加入到队列里了
}
2.4、ScheduledThreadPoolExecutor的run方法
在addWorker里面进行一系列的状态判断,最终启动线程,由于刚才在创建线程的时候传入的参数为this,所以需要执行ScheduledThreadPoolExecutor里面的run方法。
public void run() {
// 取出周期值
boolean periodic = isPeriodic();
// 判断当前线程的状态是否正常
if (!canRunInCurrentRunState(periodic))
cancel(false);
// 如果不是周期的走这个,只执行一次的那种
else if (!periodic)
ScheduledFutureTask.super.run();
// 如果是周期的,走这个,循环执行的那种
// ScheduledFutureTask.super.runAndReset() 执行具体任务并重置
else if (ScheduledFutureTask.super.runAndReset()) {
// 下一次执行时间
setNextRunTime();
// 为下一次执行做准备
reExecutePeriodic(outerTask);
}
}
2.5、reExecutePeriodic
void reExecutePeriodic(RunnableScheduledFuture<?> task) {
if (canRunInCurrentRunState(true)) {
// 添加到队列
super.getQueue().add(task);
// 判断线程状态是否正常
if (!canRunInCurrentRunState(true) && remove(task))
task.cancel(false);
else
// 继续走2.3
ensurePrestart();
}
}
10、辅助知识
10.1、当任务中抛出异常时各定时任务的处理
10.1.1、ScheduledThreadPoolExecutor的处理
private static Logger logger = Logger.getLogger(Log.class.getName());
public static void main(String[] args) {
ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(1);
logger.info("开始的时候!");
scheduledThreadPoolExecutor.scheduleWithFixedDelay(new Runnable() {
@Override
public void run() {
logger.info("执行。。。。");
throw new RuntimeException();
}
},1000,2000,TimeUnit.MILLISECONDS);
}
执行截图如下:
我们会发现线程就卡在这里了,这是因为在线程池底层给我们把异常捕获到,然后另外创建了一个线程,虽然说线程池还存在,线程也存在,但是任务却被丢弃了,所以显示就是卡在这里了。
10.1.2、Timer的处理
代码示例如下:
private static Logger logger = Logger.getLogger(Log.class.getName());
public static void main(String[] args) {
ScheduledThreadPoolExecutor scheduledThreadPoolExecutor = new ScheduledThreadPoolExecutor(1);
logger.info("开始的时候!");
Timer timer = new Timer();
timer.scheduleAtFixedRate(new TimerTask() {
@Override
public void run() {
logger.info("执行。。。。");
throw new RuntimeException();
}
},1000,2000);
}
执行截图如下:
我们会发现直接报错了,程序都已经停止运行了,因为它直接把线程给了一个成员变量,所以抛出了一个异常导致问题挺严重的,所以很多规范也不提倡我们用Timer。