这篇文章主要说说DelayedWorkQueue。

在ScheduledThreadPoolExecutor使用DelayedWorkQueue来存放要执行的任务,因为这些任务是带有延迟的,而每次执行都是取第一个任务执行,因此在DelayedWorkQueue中任务必然按照延迟时间从短到长来进行排序的。

DelayedWorkQueue使用堆来实现的。

和以前分析BlockingQueue的实现类一样,首先来看offer方法,基本就是一个添加元素到堆的逻辑。

        public boolean offer(Runnable x) {
if (x == null)
throw new NullPointerException();
RunnableScheduledFuture e = (RunnableScheduledFuture)x;
final ReentrantLock lock = this.lock;
lock.lock();
try {
int i = size;
// 因为元素时存储在一个数组中,随着堆变大,当数组存储不够时,需要对数组扩容
if (i >= queue.length)
grow();
size = i + 1;
// 如果原来队列为空
if (i == 0) {
queue[0] = e; // 这个i就是RunnableScheduledFuture用到的heapIndex
setIndex(e, 0);
} else {
// 添加元素到堆中
siftUp(i, e);
}
// 如果队列原先为空,那么可能有线程在等待元素,这时候既然添加了元
// 素,就需要通过Condition通知这些线程
if (queue[0] == e) {
// 因为有元素新添加了,第一个等待的线程可以结束等待了,因此这里
// 删除第一个等待线程
leader = null;
available.signal();
}
} finally {
lock.unlock();
}
return true;
}

这里顺带看一下siftUp,熟悉堆的实现的朋友应该很容易看懂这是一个把元素添加已有堆中的算法。

        private void siftUp(int k, RunnableScheduledFuture key) {
while (k > 0) {
int parent = (k - 1) >>> 1;
RunnableScheduledFuture e = queue[parent];
if (key.compareTo(e) >= 0)
break;
queue[k] = e;
setIndex(e, k);
k = parent;
}
queue[k] = key;
setIndex(key, k);
}

那么接着就看看poll:

        public RunnableScheduledFuture poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 因为即使拿到任务,线程还是需要等待,而这个等待过程是由队列帮助完成的
// 因此poll方法只能返回已经到执行时间点的任务
RunnableScheduledFuture first = queue[0];
if (first == null || first.getDelay(TimeUnit.NANOSECONDS) > 0)
return null;
else
return finishPoll(first);
} finally {
lock.unlock();
}
}

因为poll方法只能返回已经到了执行时间点的任务,所以对于我们理解队列如何实现延迟执行没有意义,因此重点看看take方法:

        public RunnableScheduledFuture take() throws InterruptedException {
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
for (;;) {
// 尝试获取第一个元素,如果队列为空就进入等待
RunnableScheduledFuture first = queue[0];
if (first == null)
available.await();
else {
// 获取任务执行的延迟时间
long delay = first.getDelay(TimeUnit.NANOSECONDS);
// 如果任务不用等待,立刻返回该任务给线程
if (delay <= 0)
// 从堆中拿走任务
return finishPoll(first);
// 如果任务需要等待,而且前面有个线程已经等待执行任务(leader线程
// 已经拿到任务了,但是执行时间没有到,延迟时间肯定是最短的),
// 那么执行take的线程肯定继续等待,
else if (leader != null)
available.await();
// 当前线程的延迟时间是最短的情况,那么更新leader线程
// 用Condition等待直到时间到点,被唤醒或者被中断
else {
Thread thisThread = Thread.currentThread();
leader = thisThread;
try {
available.awaitNanos(delay);
} finally {
// 重置leader线程以便进行下一次循环
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
// 队列不为空发出signal很好理解,这里附带了没有leader线程
// 的条件是因为leader线程存在时表示leader线程正在等待执行时间点的
// 到来,如果此时发出signal会触发awaitNanos提前返回
if (leader == null && queue[0] != null)
available.signal();
lock.unlock();
}
}

take方法的重点就是leader线程,因为存在延迟时间,即使拿到任务,线程还是需要等待的,leader线程就那个最先执行任务的线程。

因为线程拿到任务之后还是需要等待一段延迟执行的时间,所以对于超时等待的poll方法来说就有点意思了:

        public RunnableScheduledFuture poll(long timeout, TimeUnit unit)
throws InterruptedException {
long nanos = unit.toNanos(timeout);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
for (;;) {
RunnableScheduledFuture first = queue[0];
// 任务队列为空的情况
if (first == null) {
// nanos小于等于0有两种可能:
// 1. 参数值设定
// 2. 等待已经超时
if (nanos <= 0)
return null;
else
// 等待一段时间,返回剩余的等待时间
nanos = available.awaitNanos(nanos);
} else {
long delay = first.getDelay(TimeUnit.NANOSECONDS);
if (delay <= 0)
return finishPoll(first);
if (nanos <= 0)
return null;
// leader线程存在并且nanos大于delay的情况下,
// 依然等待nanos这么长时间,不用担心会超过delay设定
// 的时间点,因为leader线程到时间之后会发出signal
// 唤醒线程,而那个时候显然还没有到delay设定的时间点
if (nanos < delay || leader != null)
nanos = available.awaitNanos(nanos);
else {
Thread thisThread = Thread.currentThread();
leader = thisThread;
try {
long timeLeft = available.awaitNanos(delay);
// 剩余的超时时间
nanos -= delay - timeLeft;
} finally {
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
if (leader == null && queue[0] != null)
available.signal();
lock.unlock();
}
}

通过分析以上代码基本上已经理清楚了DelayedWorkQueue实现延迟执行的原理:

1. 按照执行延迟从短到长的顺序把任务存储到堆;

2. 通过leader线程让拿到任务的线程等到规定的时间点再执行任务;

《java.util.concurrent 包源码阅读》15 线程池系列之ScheduledThreadPoolExecutor 第二部分的更多相关文章

  1. 《java.util.concurrent 包源码阅读》 结束语

    <java.util.concurrent 包源码阅读>系列文章已经全部写完了.开始的几篇文章是根据自己的读书笔记整理出来的(当时只阅读了部分的源代码),后面的大部分都是一边读源代码,一边 ...

  2. 《java.util.concurrent 包源码阅读》13 线程池系列之ThreadPoolExecutor 第三部分

    这一部分来说说线程池如何进行状态控制,即线程池的开启和关闭. 先来说说线程池的开启,这部分来看ThreadPoolExecutor构造方法: public ThreadPoolExecutor(int ...

  3. 《java.util.concurrent 包源码阅读》04 ConcurrentMap

    Java集合框架中的Map类型的数据结构是非线程安全,在多线程环境中使用时需要手动进行线程同步.因此在java.util.concurrent包中提供了一个线程安全版本的Map类型数据结构:Concu ...

  4. 《java.util.concurrent 包源码阅读》02 关于java.util.concurrent.atomic包

    Aomic数据类型有四种类型:AomicBoolean, AomicInteger, AomicLong, 和AomicReferrence(针对Object的)以及它们的数组类型, 还有一个特殊的A ...

  5. 《java.util.concurrent 包源码阅读》17 信号量 Semaphore

    学过操作系统的朋友都知道信号量,在java.util.concurrent包中也有一个关于信号量的实现:Semaphore. 从代码实现的角度来说,信号量与锁很类似,可以看成是一个有限的共享锁,即只能 ...

  6. 《java.util.concurrent 包源码阅读》06 ArrayBlockingQueue

    对于BlockingQueue的具体实现,主要关注的有两点:线程安全的实现和阻塞操作的实现.所以分析ArrayBlockingQueue也是基于这两点. 对于线程安全来说,所有的添加元素的方法和拿走元 ...

  7. 《java.util.concurrent 包源码阅读》09 线程池系列之介绍篇

    concurrent包中Executor接口的主要类的关系图如下: Executor接口非常单一,就是执行一个Runnable的命令. public interface Executor { void ...

  8. 《java.util.concurrent 包源码阅读》24 Fork/Join框架之Work-Stealing

    仔细看了Doug Lea的那篇文章:A Java Fork/Join Framework 中关于Work-Stealing的部分,下面列出该算法的要点(基本是原文的翻译): 1. 每个Worker线程 ...

  9. 《java.util.concurrent 包源码阅读》25 Fork/Join框架之Fork与Work-Stealing(重写23,24)

    在写前面两篇文章23和24的时候自己有很多细节搞得不是很明白,这篇文章把Fork和Work-Stealing相关的源代码重新梳理一下. 首先来看一些线程池定义的成员变量: 关于scanGuard: v ...

  10. 《java.util.concurrent 包源码阅读》22 Fork/Join框架的初体验

    JDK7引入了Fork/Join框架,所谓Fork/Join框架,个人解释:Fork分解任务成独立的子任务,用多线程去执行这些子任务,Join合并子任务的结果.这样就能使用多线程的方式来执行一个任务. ...

随机推荐

  1. Thinking in React Implemented by Reagent

    前言  本文是学习Thinking in React这一章后的记录,并且用Reagent实现其中的示例. 概要 构造恰当的数据结构 从静态非交互版本开始 追加交互代码 一.构造恰当的数据结构 Sinc ...

  2. Linux 可运行进程 Runnable Process Definition

    From : http://www.linfo.org/runnable_process.html 一个可运行的进程是指该进程的进程状态为TASK_RUNNING. 进程,也可被称为任务,是指一个程序 ...

  3. ASP.NET Web API 2中的错误处理

    前几天在webapi项目中遇到一个问题:Controller构造函数中抛出异常时全局过滤器捕获不到,于是网搜一把写下这篇博客作为总结. HttpResponseException 通常在WebAPI的 ...

  4. PLSQL Developer oracle导入导出表及数据

    1.进入PLSQL Developer 2.创建新用户(如需要新表空间则需先创建新表空间再创建用户) 3.打开菜单Tools->Export User Objects 导出表及视图等创建SQL ...

  5. fio2.1.10--HOWTO

    1.0 Overview and history    ------------------------ fio was originally written to save me the hassl ...

  6. python第四课——线程、进程、协程

    面试or笔试题:简述线程.进程.协程之间的关系? 内容概要 1.进程与线程优.缺点的比较 2.适用情况 3.线程 线程的创建 setDaemon join event RLock 队列 4.进程 创建 ...

  7. 23种设计模式JAVA 实现目录总结

    曾看了不少的有关设计模式的文章,有的提供的实现在现在看来是有些问题,所以现在对以前看过的有关设计模式的文章在这里总结一下,随笔中有引用其他资料,并根据自己的理解重新实现了一次,23种设计模式中,并没有 ...

  8. 开源的JavaScript插件化框架MinimaJS

    本文介绍我开发的一个JavaScript编写的插件化框架——MinimaJS,完全开源,源码下载地址:https://github.com/lorry2018/minimajs.该框架参考OSGi规范 ...

  9. LeetCode 31. Next Permutation (下一个排列)

    Implement next permutation, which rearranges numbers into the lexicographically next greater permuta ...

  10. R学习笔记 第四篇:函数,分支和循环

    变量用于临时存储数据,而函数用于操作数据,实现代码的重复使用.在R中,函数只是另一种数据类型的变量,可以被分配,操作,甚至把函数作为参数传递给其他函数.分支控制和循环控制,和通用编程语言的风格很相似, ...