文本将主要讲述 ThreadPoolExecutor 一个特殊的子类 ScheduledThreadPoolExecutor,主要用于执行周期性任务;所以在看本文之前最好先了解一下 ThreadPoolExecutor ,可以参考 ThreadPoolExecutor 详解;另外 ScheduledThreadPoolExecutor 中使用了延迟队列,主要是基于完全二叉堆实现的,可以参考 完全二叉堆

一、ScheduledThreadPoolExecutor 结构概述

1. 继承关系

public class ScheduledThreadPoolExecutor
extends ThreadPoolExecutor implements ScheduledExecutorService {}

在源码中可以看到,ScheduledThreadPoolExecutor 的状态管理、入队操作、拒绝操作等都是继承于 ThreadPoolExecutorScheduledThreadPoolExecutor 主要是提供了周期任务和延迟任务相关的操作;

  • schedule(Runnable command, long delay, TimeUnit unit) // 无返回值的延迟任务
  • schedule(Callable callable, long delay, TimeUnit unit) // 有返回值的延迟任务
  • scheduleAtFixedRate(Runnable command, long initialDelay, long period, TimeUnit unit) // 固定频率周期任务
  • scheduleWithFixedDelay(Runnable command, long initialDelay, long delay, TimeUnit unit) // 固定延迟周期任务

就 `ScheduledThreadPoolExecutor` 的运行逻辑而言,大致可以表述为:

  • 首先将 Runnable/Callable 封装为 ScheduledFutureTask,延迟时间作为比较属性;
  • 然后加入 DelayedWorkQueue 队列中,每次取出队首延迟最小的任务,超时等待,然后执行;
  • 最后判断是否为周期任务,然后将其重新加入 DelayedWorkQueue 队列中;

其内部结构如图所示:

这里需要注意的:

  • ScheduledThreadPoolExecutor 中的队列不能指定,只能是 DelayedWorkQueue;因为他是 无界队列,所以再添加任务的时候线程最多可以增加到 coreSize,这里不清楚的可以查看 ThreadPoolExecutor 详解 ,就不再重复了;
  • ScheduledThreadPoolExecutor 重写了 ThreadPoolExecutor 的 execute() 方法,其执行的核心方法变成 delayedExecute()

2. ScheduledFutureTask

private class ScheduledFutureTask<V> extends FutureTask<V> implements RunnableScheduledFuture<V> {
private final long sequenceNumber; // 任务序号,从 AtomicLong sequencer 获取,当延迟时间相同时,序号小的先出
private long time; // 下次任务执行时间
private final long period; // 0 表示非周期任务,正值表示固定频率周期任务,负值表示固定延迟周期任务
RunnableScheduledFuture<V> outerTask = this; // 重复执行的任务,传入的任务可以使用 decorateTask() 重新包装
int heapIndex; // 队列索引
}

其中最重要的方法必然是 run 方法了:

public void run() {
boolean periodic = isPeriodic(); // 是否为周期任务,period != 0
if (!canRunInCurrentRunState(periodic)) // 当前状态能否继续运行,详细测试后面还会讲到
cancel(false); // 取消任务
else if (!periodic) // 不是周期任务时,直接运行
ScheduledFutureTask.super.run();
else if (ScheduledFutureTask.super.runAndReset()) { // 时周期任务
setNextRunTime(); // 设置下次执行时间
reExecutePeriodic(outerTask); // 重新入队
}
}
public boolean cancel(boolean mayInterruptIfRunning) {
boolean cancelled = super.cancel(mayInterruptIfRunning); // 设置中断状态
if (cancelled && removeOnCancel && heapIndex >= 0) // 当设置 removeOnCancel 状态时,移除任务
remove(this); // 默认为 false
return cancelled;
}
void reExecutePeriodic(RunnableScheduledFuture<?> task) {
if (canRunInCurrentRunState(true)) { // 如果当前状态可以执行
super.getQueue().add(task); // 则重新入队
if (!canRunInCurrentRunState(true) && remove(task))
task.cancel(false);
else ensurePrestart(); // 确保有线程执行任务
}
}

此外还有 DelayedWorkQueue,但是这里不准备讲了,可以查看 完全二叉堆 了解实现的原理;

二、scheduleAtFixedRate 与 scheduleWithFixedDelay

scheduleAtFixedRatescheduleWithFixedDelay 是我们最常用的两个方法,但是他们的区别可能不是很清楚,这里重点讲一下,

1. scheduleAtFixedRate

// 测试
ScheduledThreadPoolExecutor pool = new ScheduledThreadPoolExecutor(1);
pool.scheduleAtFixedRate(() -> {
sleep(1000); // 睡眠 1s,
log.info("run task");
}, 1, 2, TimeUnit.SECONDS); // 延迟 1s,周期 2s

// 打印

[19:41:28,489 INFO ] [pool-1-thread-1] - run task

[19:41:30,482 INFO ] [pool-1-thread-1] - run task

[19:41:32,483 INFO ] [pool-1-thread-1] - run task

[19:41:34,480 INFO ] [pool-1-thread-1] - run task

可以看到的确时固定周期 2s 执行的,但是如果任务执行时间超过周期呢?

// 测试
ScheduledThreadPoolExecutor pool = new ScheduledThreadPoolExecutor(1);
pool.scheduleAtFixedRate(() -> {
int i = 2000 + random.nextInt(3) * 1000;
sleep(i);
log.info("run task, sleep :{}", i);
}, 1, 2, TimeUnit.SECONDS); // 延迟 1s,周期 2s

// 打印

[19:42:53,428 INFO ] [pool-1-thread-1] - run task, sleep :2000

[19:42:55,430 INFO ] [pool-1-thread-1] - run task, sleep :2000

[19:42:59,430 INFO ] [pool-1-thread-1] - run task, sleep :4000

[19:43:02,434 INFO ] [pool-1-thread-1] - run task, sleep :3000

[19:43:06,436 INFO ] [pool-1-thread-1] - run task, sleep :4000

可以看到如果任务执行时间超出周期时,下一次任务会立刻运行;就好像周期是一个有弹性的袋子,能装下运行时间的时候,是固定大小,装不下的时候就会被撑大,图像化表示如下:

2. scheduleWithFixedDelay

// 测试
ScheduledThreadPoolExecutor pool = new ScheduledThreadPoolExecutor(1);
pool.scheduleAtFixedRate(() -> {
int i = 1000 + random.nextInt(5) * 1000;
sleep(i);
log.info("run task, sleep :{}", i);
}, 1, 2, TimeUnit.SECONDS); // 延迟 1s,周期 2s

// 打印

[20:05:40,682 INFO ] [pool-1-thread-1] - run task, sleep :1000

[20:05:45,686 INFO ] [pool-1-thread-1] - run task, sleep :3000

[20:05:49,689 INFO ] [pool-1-thread-1] - run task, sleep :2000

[20:05:55,690 INFO ] [pool-1-thread-1] - run task, sleep :4000

[20:06:01,692 INFO ] [pool-1-thread-1] - run task, sleep :4000

可以看到无论执行时间是多少,其结果都是在执行完毕后,停顿固定的时间,然后执行下一次任务,其图形化表示为:

三、 源码分析

1. 延迟任务

public void execute(Runnable command) {
schedule(command, 0, NANOSECONDS);
} public <T> Future<T> submit(Callable<T> task) {
return schedule(task, 0, NANOSECONDS);
} public ScheduledFuture<?> schedule(Runnable command, long delay, TimeUnit unit) {
if (command == null || unit == null) throw new NullPointerException();
RunnableScheduledFuture<?> t = decorateTask(
command,new ScheduledFutureTask<Void>(command, null, triggerTime(delay, unit)));
delayedExecute(t);
return t;
} public <V> ScheduledFuture<V> schedule(Callable<V> callable, long delay, TimeUnit unit) {
if (callable == null || unit == null) throw new NullPointerException();
RunnableScheduledFuture<V> t = decorateTask(
callable, new ScheduledFutureTask<V>(callable, triggerTime(delay, unit)));
delayedExecute(t);
return t;
}

可以看到所有的周期任务,最终执行的都是 delayedExecute 方法,其中 decorateTask 是一个钩子函数,其之类可以利用他对任务进行重构过滤等操作;

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. 周期任务

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);
sft.outerTask = t;
delayedExecute(t);
return t;
} public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command, long initialDelay,
long delay, TimeUnit unit) {
if (command == null || unit == null) throw new NullPointerException();
if (delay <= 0) throw new IllegalArgumentException(); ScheduledFutureTask<Void> sft =
new ScheduledFutureTask<Void>(command,
null,
triggerTime(initialDelay, unit),
unit.toNanos(-delay)); // 注意这里添加的是负值 RunnableScheduledFuture<Void> t = decorateTask(command, sft);
sft.outerTask = t;
delayedExecute(t);
return t;
}

从上面代码可以看到 scheduleAtFixedRatescheduleWithFixedDelay 只有周期任务的时间不同,其他的都一样,那么下面我们看一下他们的任务时间计算;

public long getDelay(TimeUnit unit) {
return unit.convert(time - now(), NANOSECONDS);
} private void setNextRunTime() {
long p = period;
if (p > 0) // 正值表示 scheduleAtFixedRate
time += p; // 不管任务执行时间,直接加上周期时间,也就是一次任务超时,会影响后续任务的执行,
// 超时的时候,getDelay 是负值,所以在延迟队列中必然排在最前面,立刻被取出执行
else
time = triggerTime(-p); // 计算触发时间
} long triggerTime(long delay) { // 这里可以看到,每次的确是在当前时间的基础上,加上延迟时间;
return now() + ((delay < (Long.MAX_VALUE >> 1)) ? delay : overflowFree(delay));
}

这里特别要注意 scheduleAtFixedRate 一次任务超时,会持续影响后面的任务周期安排,所以在设定周期的时候要特别注意; 例如:

// 测试
ScheduledThreadPoolExecutor pool = new ScheduledThreadPoolExecutor(1);
pool.scheduleAtFixedRate(() -> {
int i = random.nextInt(5) * 1000;
sleep(i);
log.info("run task, sleep :{}", i);
}, 1, 2, TimeUnit.SECONDS);

// 打印

[20:29:11,310 INFO ] [pool-1-thread-1] - run task, sleep :1000

[20:29:16,304 INFO ] [pool-1-thread-1] - run task, sleep :4000

[20:29:19,304 INFO ] [pool-1-thread-1] - run task, sleep :3000

[20:29:21,305 INFO ] [pool-1-thread-1] - run task, sleep :2000

[20:29:22,305 INFO ] [pool-1-thread-1] - run task, sleep :1000

[20:29:23,306 INFO ] [pool-1-thread-1] - run task, sleep :1000

[20:29:27,306 INFO ] [pool-1-thread-1] - run task, sleep :4000

[20:29:30,307 INFO ] [pool-1-thread-1] - run task, sleep :3000

如图所示:

3. 取消任务

private volatile boolean continueExistingPeriodicTasksAfterShutdown; //关闭后继续执行周期任务,默认false
private volatile boolean executeExistingDelayedTasksAfterShutdown = true; //关闭后继续执行延迟任务,默认true
private volatile boolean removeOnCancel = false; // 取消任务是,从队列中删除任务,默认 false @Override void onShutdown() {
BlockingQueue<Runnable> q = super.getQueue();
boolean keepDelayed = getExecuteExistingDelayedTasksAfterShutdownPolicy(); // 继续延迟任务
boolean keepPeriodic = getContinueExistingPeriodicTasksAfterShutdownPolicy(); // 继续周期任务
if (!keepDelayed && !keepPeriodic) { // 都是 false,直接清除
for (Object e : q.toArray())
if (e instanceof RunnableScheduledFuture<?>)
((RunnableScheduledFuture<?>) e).cancel(false);
q.clear();
}
else {
// Traverse snapshot to avoid iterator exceptions
for (Object e : q.toArray()) {
if (e instanceof RunnableScheduledFuture) {
RunnableScheduledFuture<?> t = (RunnableScheduledFuture<?>)e;
if ((t.isPeriodic() ? !keepPeriodic : !keepDelayed) ||
t.isCancelled()) { // also remove if already cancelled
if (q.remove(t))
t.cancel(false);
}
}
}
}
tryTerminate();
}

总结

  • scheduleAtFixedRate,固定频率周期任务,注意一次任务超时,会持续的影响后续的任务周期;
  • scheduleWithFixedDelay,固定延迟周期任务,即每次任务结束后,超时等待固定时间;
  • 此外 ScheduledThreadPoolExecutor 线程最多为核心线程,最大线程数不起作用,因为 DelayedWorkQueue 是无界队列;

并发系列(7)之 ScheduledThreadPoolExecutor 详解的更多相关文章

  1. 【Java并发系列03】ThreadLocal详解

    img { border: solid 1px } 一.前言 ThreadLocal这个对象就是为多线程而生的,没有了多线程ThreadLocal就没有存在的必要了.可以将任何你想在每个线程独享的对象 ...

  2. 高并发架构系列:Redis并发竞争key的解决方案详解

    https://blog.csdn.net/ChenRui_yz/article/details/85096418 https://blog.csdn.net/ChenRui_yz/article/l ...

  3. C++11 并发指南三(std::mutex 详解)

    上一篇<C++11 并发指南二(std::thread 详解)>中主要讲到了 std::thread 的一些用法,并给出了两个小例子,本文将介绍 std::mutex 的用法. Mutex ...

  4. ASP.NET MVC深入浅出系列(持续更新) ORM系列之Entity FrameWork详解(持续更新) 第十六节:语法总结(3)(C#6.0和C#7.0新语法) 第三节:深度剖析各类数据结构(Array、List、Queue、Stack)及线程安全问题和yeild关键字 各种通讯连接方式 设计模式篇 第十二节: 总结Quartz.Net几种部署模式(IIS、Exe、服务部署【借

    ASP.NET MVC深入浅出系列(持续更新)   一. ASP.NET体系 从事.Net开发以来,最先接触的Web开发框架是Asp.Net WebForm,该框架高度封装,为了隐藏Http的无状态模 ...

  5. C++11 并发指南三(std::mutex 详解)(转)

    转自:http://www.cnblogs.com/haippy/p/3237213.html 上一篇<C++11 并发指南二(std::thread 详解)>中主要讲到了 std::th ...

  6. 【C/C++开发】C++11 并发指南三(std::mutex 详解)

    本系列文章主要介绍 C++11 并发编程,计划分为 9 章介绍 C++11 的并发和多线程编程,分别如下: C++11 并发指南一(C++11 多线程初探)(本章计划 1-2 篇,已完成 1 篇) C ...

  7. 分布式-技术专区-Redis并发竞争key的解决方案详解

    Redis缓存的高性能有目共睹,应用的场景也是非常广泛,但是在高并发的场景下,也会出现问题:缓存击穿.缓存雪崩.缓存和数据一致性,以及今天要谈到的缓存并发竞争.这里的并发指的是多个redis的clie ...

  8. C++11 并发指南六(atomic 类型详解四 C 风格原子操作介绍)

    前面三篇文章<C++11 并发指南六(atomic 类型详解一 atomic_flag 介绍)>.<C++11 并发指南六( <atomic> 类型详解二 std::at ...

  9. C++11 并发指南六(atomic 类型详解三 std::atomic (续))

    C++11 并发指南六( <atomic> 类型详解二 std::atomic ) 介绍了基本的原子类型 std::atomic 的用法,本节我会给大家介绍C++11 标准库中的 std: ...

  10. C++11 并发指南六( <atomic> 类型详解二 std::atomic )

    C++11 并发指南六(atomic 类型详解一 atomic_flag 介绍)  一文介绍了 C++11 中最简单的原子类型 std::atomic_flag,但是 std::atomic_flag ...

随机推荐

  1. 重温《STL源码剖析》笔记 第三章

    源码之前,了无秘密. --侯杰 第三章:迭代器概念与traits编程技法 迭代器是一种smart pointer auto_Ptr 是一个用来包装原生指针(native pointer)的对象,声明狼 ...

  2. 学习Android过程中的一些博客或工具收集

    android studio中使用SlidingMenu: 超简单Android Studio导入第三方库(SlidingMenu)教程绝对傻瓜版 android 更新sdk23以后,报错提示Floa ...

  3. zabbix 3.4 ubuntu 16 用腾讯企业邮箱作为告警邮箱

    最近一直在研究zabbix监控系统,今天调试了腾讯企业邮箱作为告警邮箱的设置,本次方式是用内置email组件. 第一步: 选择Administration-->Media Types--> ...

  4. goroutine和线程区别

    从调度上看,goroutine的调度开销远远小于线程调度开销. OS的线程由OS内核调度,每隔几毫秒,一个硬件时钟中断发到CPU,CPU调用一个调度器内核函数.这个函数暂停当前正在运行的线程,把他的寄 ...

  5. [ 搭建Redis本地服务器实践系列二 ] :图解CentOS7配置Redis

    上一章 [ 搭建Redis本地服务器实践系列一 ] :图解CentOS7安装Redis 详细的介绍了Redis的安装步骤,那么只是安装完成,此时的Redis服务器还无法正常运作,我们需要对其进行一些配 ...

  6. Unity3D学习(四):小游戏Konster的整体代码重构

    前言 翻了下之前写的代码,画了个图看了下代码结构,感觉太烂了,有很多地方的代码重复啰嗦,耦合也紧,开个随笔记录下重构的过程. 过程 _____2017.10.13_____ 结构图: 目前发现的待改进 ...

  7. oracle批量插入测试数据

    做数据库开发或管理的人经常要创建大量的测试数据,动不动就需要上万条,如果一条一条的录入,那会浪费大量的时间,本文介绍了Oracle中如何通过一条 SQL快速生成大量的测试数据的方法.产生测试数据的SQ ...

  8. 对于spring中事务@Transactional注解的理解

    现在spring的配置都喜欢用注解,这边就说下@Transactional 一.如何开启@Transactional支持 要使用@Transactional,spring的配置文件applicatio ...

  9. iOS常用控件尺寸大集合

    元素控件 尺寸(pts) Window(含状态栏) 320 x 480 Status Bar的高度 20 Navigation Bar的高度 44 含Prompt的Navigation Bar的高度 ...

  10. 超实用的JavaScript代码段 Item3 --图片轮播效果

    图片轮播效果 图片尺寸 统一设置成:490*170px; 一.页面加载.获取整个容器.所有放数字索引的li及放图片列表的ul.定义放定时器的变量.存放当前索引的变量index 二.添加定时器,每隔2秒 ...