学习线程池源码--ScheduledThreadPoolExecutor
1. 创建ScheduledThreadPoolExecutor
ScheduledThreadPoolExecutor继承自ThreadPoolExecutor,实现了ScheduledExecutorService接口,在ThreadPoolExecutor的基础上增加了定时的功能,包括指定延时后执行任务和指定延时后执行任务。ScheduledThreadPoolExecutor的功能与Timer类似,但ScheduledThreadPoolExecutor功能更强大、更灵活。Timer对应的是单个后台线程,而ScheduledThreadPoolExecutor可以多线程执行,所以ScheduledThreadPoolExecutor是比Timer更优的选择。
public class ScheduledThreadPoolExecutor
extends ThreadPoolExecutor
implements ScheduledExecutorService {
public ScheduledThreadPoolExecutor(int corePoolSize,
ThreadFactory threadFactory,
RejectedExecutionHandler handler) {
super(corePoolSize, Integer.MAX_VALUE, 0, NANOSECONDS,
new DelayedWorkQueue(), threadFactory, handler);
}
}
从构造函数可以看出ScheduledThreadPoolExecutor还是调用的父类ThreadPoolExecutor的构造方式,其中最大线程数固定为Integer.MAX_VALUE,空闲时间固定为0,工作队列固定为DelayedWorkQueue。DelayedWorkQueue是一个无界队列,故最大线程数并没有什么意义。
Executors工具类提供了几种ScheduledThreadPoolExecutor的创建方法:
1. newScheduledThreadPool
只指定核心线程数,使用默认的线程工厂以及拒绝策略,最多创建corePoolSize个线程。
public static ScheduledExecutorService newScheduledThreadPool(int corePoolSize) {
return new ScheduledThreadPoolExecutor(corePoolSize);
}
2. newSingleThreadScheduledExecutor
核心线程数为1,使用默认的线程工厂以及拒绝策略,单线程的newScheduledThreadPool。
public static ScheduledExecutorService newSingleThreadScheduledExecutor() {
return new DelegatedScheduledExecutorService
(new ScheduledThreadPoolExecutor(1));
}
2. 核心思想
ScheduledThreadPoolExecutor的基本实现依赖于ThreadPoolExecutor,只不过用了特殊的工作队列DelayedWorkQueue(一种基于堆实现的优先队列,类似PriorityQueue),DelayedWorkQueue的队首始终为执行时间最早的一个任务,当worker线程调用DelayedWorkQueue.take()方法时,如果已有leader线程则当前worker线程会成为follower线程进入等待状态(leader/follower多线程网络模型,后面在介绍DelayedWorkQueue时有说明),如果不存在leader线程则当前worker线程会成为leader线程,如果当前时间小于执行时间,则leader线程会阻塞直到当前时间大于等于执行时间才将队首的任务返回,然后leader线程就执行该任务并交出leader权限提拔一个follower线程成为新的leader。如果是周期性任务,则执行后根据执行频率重新计算下次执行时间后再放入DelayedWorkQueue中。
3. 核心方法
1. schedule方法
schedule有两个重载方法,一个用于执行Runnable任务,一个用于执行Callable任务,command为要执行的任务,delay为延时执行的时间,unit为延时执行的时间单位。二者都是先将任务封装为一个RunnableScheduledFuture对象(实际上就是ScheduledFutureTask),然后放入DelayedWorkQueue队列中,等待时间线程池中的线程从队列中获取任务并执行。Callable与Runnable的区别在于,Callable可以通过调用FutureTask.get()来获取执行结果,不过该方法会阻塞主线程直到获得结果。
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;
}
2. scheduleAtFixedRate方法
scheduleAtFixedRate用于执行固定频率(时间间隔)的任务,command为要执行的任务,initialDelay为第一次执行的延迟时间,period为后面每次执行的时间间隔,unit为时间单位。与schedule方法一样,scheduleAtFixedRate方法也是通过创建一个RunnableScheduledFuture对象(实际上就是ScheduledFutureTask),然后放入DelayedWorkQueue队列中,等待时间线程池中的线程从队列中获取任务并执行。区别在于首次调用scheduleAtFixedRate方法时会在initialDelay时间之后首次执行任务,之后每次根据当前执行时间延迟period时间执行,所以每次执行的时间大致是能确定的。
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;
}
3. scheduleWithFixedDelay方法
scheduleWithFixedDelay也是用于执行固定频率的任务,与scheduleAtFixedRate方法不同的是,scheduleWithFixedDelay是在任务执行完成之后的时间加上delay时间延迟执行的,所以每次执行的时间不是确定的(因为不知道任务要执行多久),但间隔是确定的。其他的同scheduleAtFixedRate方法。
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;
}
4. delayedExecute方法
因为ScheduledThreadPoolExecutor是用于执行定时或周期性任务的线程池,所以每次提交任务都是直接放入工作队列而不是直接执行,故每次提交任务会创建空的工作线程直到线程池中的线程数达到核心线程数,然后每个线程都是通过循环调用take()方法去队列中获取任务然后执行。
private void delayedExecute(RunnableScheduledFuture<?> task) {
//如果线程池已停止运行,则执行拒绝策略
if (isShutdown())
reject(task);
else {
//将任务加入工作队列
super.getQueue().add(task);
//再次确认线程池状态,如果是停止状态则将task从工作队列移除,然后取消任务但不中断执行线程
if (isShutdown() &&
!canRunInCurrentRunState(task.isPeriodic()) &&
remove(task))
task.cancel(false);
else
// 该方法在ThreadPoolExecutor中实现,是为了确保线程池中至少有一个线程启动,即使corePoolSize为0
// 在这里是每次添加任务都会创建一个线程,直到线程池中的线程数达到核心线程数
ensurePrestart();
}
}
4. 重要内部类
1. ScheduledFutureTask
ScheduledFutureTask继承自FutureTask实现了RunnableScheduledFuture接口,它的time属性代表了该任务的执行时间,sequenceNumber代表了该任务加入ScheduledThreadPoolExecutor的序号,period代表了任务执行的时间间隔。
private class ScheduledFutureTask<V>
extends FutureTask<V> implements RunnableScheduledFuture<V> {
//添加到ScheduledThreadPoolExecutor中的序号
private final long sequenceNumber;
//这个任务要被执行的具体时间
private long time;
//任务执行的时间间隔
private final long period;
/** The actual task to be re-enqueued by reExecutePeriodic */
RunnableScheduledFuture<V> outerTask = this;
//在DelayedWorkQueue中的索引,方便快速取消任务
int heapIndex;
}
ScheduledFutureTask重写了compareTo方法,首先通过time来比较大小,如果time相同,则跟据sequenceNumber的大小来进行判断。这里主要是为了方便DelayedWorkQueue排序用。
public int compareTo(Delayed other) {
if (other == this) // compare zero if same object
return 0;
if (other instanceof ScheduledFutureTask) {
ScheduledFutureTask<?> x = (ScheduledFutureTask<?>)other;
long diff = time - x.time;
if (diff < 0)
return -1;
else if (diff > 0)
return 1;
else if (sequenceNumber < x.sequenceNumber)
return -1;
else
return 1;
}
long diff = getDelay(NANOSECONDS) - other.getDelay(NANOSECONDS);
return (diff < 0) ? -1 : (diff > 0) ? 1 : 0;
}
2. DelayedWorkQueue
ScheduledThreadPoolExecutor之所以使用DelayedWorkQueue作为工作队列,是因为定时任务需要优先执行时间靠前的任务,所以ScheduledThreadPoolExecutor就在内部实现了一个线程安全的、阻塞的优先队列。
DelayedWorkQueue是一个基于堆的数据结构,继承自AbstractQueue实现了BlockingQueue接口。内部使用数组实现堆的功能,通过Leader/Follower多线程网络模型防止了动态内存分布及线程间的数据交换。
由于DelayedWorkQueue是优先队列,所以用最小堆实现的,即最早要执行的任务放在队列首部。
Leader/Follower多线程网络模型:最多只有一个leader线程用于监听任务,而其他的空闲中的线程(follower)都在等待成为leader,当leader获取到任务之后会首先提拔一个follower线程成为leader,然后自己去执行任务成为processor。
static class DelayedWorkQueue extends AbstractQueue<Runnable>
implements BlockingQueue<Runnable> {
//初始容量
private static final int INITIAL_CAPACITY = 16;
//底层用数组实现堆
private RunnableScheduledFuture<?>[] queue =
new RunnableScheduledFuture<?>[INITIAL_CAPACITY];
//使用可重入锁来保证线程安全
private final ReentrantLock lock = new ReentrantLock();
//队列大小
private int size = 0;
//leader线程,始终为最早要执行任务的执行线程
private Thread leader = null;
//与lock结合使用
private final Condition available = lock.newCondition();
}
1.入列方法(offer)
public boolean offer(Runnable x) {
if (x == null)
throw new NullPointerException();
RunnableScheduledFuture<?> e = (RunnableScheduledFuture<?>)x;
//获取lock实例并加锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
int i = size;
//如果容量不够,则进行扩容操作
if (i >= queue.length)
grow();
//修改队列大小
size = i + 1;
//如果原来队列为空,则直接将任务放入数组索引0处
if (i == 0) {
queue[0] = e;
setIndex(e, 0);
}
//否则根据堆的规则进行排序
else {
siftUp(i, e);
}
//如果入队的任务在队列首部,则重置leader线程
if (queue[0] == e) {
//先将leader线程置为空
leader = null;
//再重新提拔一个follower成为leader
available.signal();
}
} finally {
//释放锁
lock.unlock();
}
return true;
}
所有的入列方法都是调用这一个方法。首先判断队列容量是否足够,不够则进行扩容;然后根据最小堆的规则进行排序插入操作;最后判断加入的任务是否具有最高优先级,是则重置leader线程用于执行插入的任务。整个过程通过ReentrantLock加锁来保证线程安全。这里涉及了几个重要的方法,扩容方法grow,排序插入方法siftUp。
2. 扩容方法(grow)
private void grow() {
int oldCapacity = queue.length;
int newCapacity = oldCapacity + (oldCapacity >> 1); // grow 50%
if (newCapacity < 0) // overflow
newCapacity = Integer.MAX_VALUE;
queue = Arrays.copyOf(queue, newCapacity);
}
与ArrayList的扩容方法很相似,DelayedWorkQueue容量不够时会自动扩容,每次扩容50%,但是不能超过Integer最大值,通过Arrays.copyOf进行扩容,实际上底层调用System.arraycopy()达到高效扩容的目的。
3. 排序插入方法(siftUp)
private void siftUp(int k, RunnableScheduledFuture<?> key) {
while (k > 0) {
//取k位置的父节点索引
int parent = (k - 1) >>> 1;
//取父节点e
RunnableScheduledFuture<?> e = queue[parent];
//如果key比父节点大,则满足最小堆规则,停止循环
if (key.compareTo(e) >= 0)
break;
//如果父节点较大,则把父节点位置与key交换
queue[k] = e;
setIndex(e, k);
k = parent;
}
//通过循环确定了key的索引位置,将key插入该位置
queue[k] = key;
setIndex(key, k);
}
每次先将插入的任务放在队尾,然后通过比较大小确定最终位置。因为ScheduledFutureTask已经根据时间大小重写了compareTo()方法,所以这里直接通过compareTo()方法比较大小来进行排序。这里需要明白最小堆的规则,需要满足2个条件:1.是完全二叉树,2.父节点的值不能小于子节点的值。最小堆在数组中的表示方法如下:
// 对于n位置的节点来说:
int left = 2 * n + 1; // 左子节点
int right = 2 * n + 2; // 右子节点
int parent = (n - 1) / 2; // 父节点,当然n要大于0,根节点是没有父节点的
4. 等待获取队首方法(take),实现任务调度的核心
public RunnableScheduledFuture<?> take() throws InterruptedException {
//获取lock实例并加锁
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
for (;;) {
//获取队首任务
RunnableScheduledFuture<?> first = queue[0];
//如果队首任务为空,进入等待状态
if (first == null)
available.await();
else {
//获取队首任务的延时数
long delay = first.getDelay(NANOSECONDS);
//延时数小于等于0;说明任务已达调度时间,取出队列交.
给leader线程去执行。
if (delay <= 0)
return finishPoll(first);
//任务还未到执行时间,分情况进入等待状态
first = null; // don't retain ref while waiting
//如果leader线程已存在,则进入等待状态成为follower,等待提拔为leader线程
if (leader != null)
available.await();
else {
//如果leader线程不存在,则将当前线程提拔为leader线程
Thread thisThread = Thread.currentThread();
leader = thisThread;
try {
//进入等待状态直到达到任务执行时间
available.awaitNanos(delay);
} finally {
//到达执行时间,leader线程去执行任务,成为processor
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
//leader线程已成为processor,如果队首还有任务,则重新提拔一个follower线程成为leader线程
if (leader == null && queue[0] != null)
available.signal();
//释放锁
lock.unlock();
}
}
take()方法充分体现了leader/follower多线程网络模型的思想以及ReentrantLock与Condition的配合运用,也是ScheduledThreadPoolExecutor实现任务调度的关键所在。
首先通过ReentrantLock加锁,然后去取队首的任务(队首的任务由leader线程执行),如果已有leader线程则成为follower线程等待提拔为leader线程,如果没有leader线程则直接晋升为leader线程,等待达到执行时间后通过finishPoll()方法取出任务并执行,同时交出leader权限,提拔一个follower线程成为新的leader线程来执行下一个任务。
其中的follower线程等待通过Condition.await()方法实现,leader线程的等待通过Condition.awaitNanos()实现,提拔一个follower线程成为新的leader线程通过Condition.signal()实现。
DelayedWorkQueue出列的方法还有poll方法,不过线程池只用到了take()方法,take()方法中还有一个出列动作的方法finishPoll()。
5. 出列方法(finishPoll)
private RunnableScheduledFuture<?> finishPoll(RunnableScheduledFuture<?> f) {
//将队列大小-1并赋值给s
int s = --size;
//取队尾任务x
RunnableScheduledFuture<?> x = queue[s];
//将队尾任务置为空
queue[s] = null;
//如果队尾和队首不是同一个任务,则进行移除排序
if (s != 0)
siftDown(0, x);
//将f在堆中的索引设为-1代表已取出,然后将任务返回
setIndex(f, -1);
return f;
}
take()方法会阻塞线程直到任务达到执行时间,达到执行时间后就会调用finishPoll()方法将任务取出队列。取出的思路是将队尾的任务放到队首,然后通过siftDown()进行重排序。
6. 移除排序方法(siftDown)
private void siftDown(int k, RunnableScheduledFuture<?> key) {
//取队列长度的一半
int half = size >>> 1;
//通过循环比较保证父节点不大于子节点
while (k < half) {
//取k的左子节点作为child
int child = (k << 1) + 1;
RunnableScheduledFuture<?> c = queue[child];
//取k的右子节点
int right = child + 1;
//如果右子节点在队列中并且左子节点大于右子节点,则取右子节点作为child
//目的是为了取左右子节点中较小的一个作为child
if (right < size && c.compareTo(queue[right]) > 0)
c = queue[child = right];
//将key与子节点中较小的一个进行比较,如果key较小,说明满足最小堆要求,终止循环
if (key.compareTo(c) <= 0)
break;
//否则将key与child交换位置,然后继续向下检查
queue[k] = c;
setIndex(c, k);
k = child;
}
queue[k] = key;
setIndex(key, k);
}
学习线程池源码--ScheduledThreadPoolExecutor的更多相关文章
- 学习线程池源码--ThreadPoolExecutor
1 创建ThreadPoolExecutor ThreadPollExecutor有四个构造函数,但本质上都是调用这一个构造函数. public ThreadPoolExecutor(int core ...
- java多线程——线程池源码分析(一)
本文首发于cdream的个人博客,点击获得更好的阅读体验! 欢迎转载,转载请注明出处. 通常应用多线程技术时,我们并不会直接创建一个线程,因为系统启动一个新线程的成本是比较高的,涉及与操作系统的交互, ...
- java多线程----线程池源码分析
http://www.cnblogs.com/skywang12345/p/3509954.html 线程池示例 在分析线程池之前,先看一个简单的线程池示例. 1 import java.util.c ...
- Java多线程学习之线程池源码详解
0.使用线程池的必要性 在生产环境中,如果为每个任务分配一个线程,会造成许多问题: 线程生命周期的开销非常高.线程的创建和销毁都要付出代价.比如,线程的创建需要时间,延迟处理请求.如果请求的到达率非常 ...
- 线程池之ScheduledThreadPoolExecutor线程池源码分析笔记
1.ScheduledThreadPoolExecutor 整体结构剖析. 1.1类图介绍 根据上面类图图可以看到Executor其实是一个工具类,里面提供了好多静态方法,根据用户选择返回不同的线程池 ...
- Java线程池源码解析
线程池 假如没有线程池,当存在较多的并发任务的时候,每执行一次任务,系统就要创建一个线程,任务完成后进行销毁,一旦并发任务过多,频繁的创建和销毁线程将会大大降低系统的效率.线程池能够对线程进行统一的分 ...
- Java并发编程中线程池源码分析及使用
当Java处理高并发的时候,线程数量特别的多的时候,而且每个线程都是执行很短的时间就结束了,频繁创建线程和销毁线程需要占用很多系统的资源和时间,会降低系统的工作效率. 参考http://www.cnb ...
- 【图灵学院10】高并发之java线程池源码分析
1. 提纲 1)线程池的模块结构 2)示例&原理解析 2. 问题 1)线程池包含哪些东西 2)线程池的运作原理 3)调度线程池的运作原理 4)线程池怎么实现FixRate,FixDelay,他 ...
- 手撕ThreadPoolExecutor线程池源码
这篇文章对ThreadPoolExecutor创建的线程池如何操作线程的生命周期通过源码的方式进行详细解析.通过对execute方法.addWorker方法.Worker类.runWorker方法.g ...
随机推荐
- 浅谈response和request方法
一:概述 Web服务器收到客户端的http请求,会针对每一次请求,分别创建一个用于代表请求的request对象.和代表响应的response对象. 按这个理解的话一次请求生成一个request和res ...
- iOS之String动态书写
/** String动画书写出来 @param string 要写的字 @param view 父视图 @param ui_font 字体大小 @param color 字体颜色 */ - (void ...
- OpenCASCADE 7.4.0 Released
Open Cascade is pleased to announce a new public release of Open CASCADE Technology (version 7.4.0). ...
- 在熟练使用2B铅笔前,请不要打开Axure
在互联网产品领域,Axure已成为产品经理.产品设计师以及交互设计师的必备工具,从某种程度讲,Axure帮助我们建立低保真模型,便于与用户的需求验证,也帮助我们构思交互细节,使前端和开发人员更容易理解 ...
- JS基础语法之DOM01(找标签、改属性样式)
DOM:暂时可以理解为找标签.改样式 1.BOM: 记住location这个就可以: location.href 获得当前页面的URL location.href = "URL" ...
- [转]WPF中的动画
WPF中的动画 周银辉 动画无疑是WP ...
- Tool-XManager:XManager(远端X窗口系统的工具)
ylbtech-Tool-XManager:XManager(远端X窗口系统的工具) Xmanager是一款小巧.便捷的浏览远端X窗口系统的工具.在工作中经常使用Xmanager来登录远端的Solar ...
- WIX、Squarespace、WordPress 三者的优劣分别是什么?
层出不穷的智能建站,模板建站,源码建站,云建站,仿站,各种建站概念都抛洒于红海之中.到底什么样的网站适合自己,什么样的网站值得我们去消费,什么样的网站能长久,是个非常值得思考的问题. 网站建设技术非常 ...
- 谈一谈创建React Component的几种方式
当我们谈起React的时候,多半会将注意力集中在组件之上,思考如何将页面划分成一个个组件,以及如何编写可复用的组件.但对于接触React不久,还没有真正用它做一个完整项目的人来说,理解如何创建一个组件 ...
- [code] if (x<0)x=0;else if (x>255)x=255;
//颜色范围0-255: // 1.原始: )tem_b=;)tem_b=; )tem_g=;)tem_g=; )tem_r=;)tem_r=; //2.使用条件状态值生成掩码来移除条件分支 tem_ ...