这篇文章主要说说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. Babel运行原理

    前言     之前翻博客园的时候,看到有人朋友分享阿里巴巴的面试题,其中有一道题就是关于ES6转ES5 原理的,当时我看到感觉到自己离去阿里巴巴的路还很远啊,像我们大部分做开发的时候,都只知其然不知 ...

  2. hdu 1018 共同交流~

    Big Number Time Limit: 2000/1000 MS (Java/Others)    Memory Limit: 65536/32768 K (Java/Others)Total ...

  3. 修改Oracle【12C】字符集

    select userenv('language') from dual; //查看系统字符集编码 select * from nls_database_parameters where parame ...

  4. LINUX 笔记-VIM常用命令整理

    1.进入insert模式 o:当前行后 O:当前行前 r:替换当前字符 R:替换当前字符直到ESC 2.删除命令 #dw:删除#个word d^:删除至行尾 d$:删除至行首 3. u:撤消 ctrl ...

  5. Linux系统网络基本配置

    1. ifconfig命令的使用: (1)查看所有网卡基本信息:ifconfig (2)查看特定网卡信息:ifconfig (网卡名,如:eht0) (3)停止网卡设备服务:ifconfig (网卡名 ...

  6. SSM框架+slf4j 以Gradle实现

    环境:win10+jdk8+tomcat9+Intellij IDEA 首先,作为一个喜欢偷懒的人,管理jar之类的的事情太累,所以用了Gradle项目管理器 第一步: 新建一个gradle-web项 ...

  7. AI翻译离无障碍交流有多远

    AI翻译服务通过硬件.软件连接千千万万个应用场景,会打破语言不通的尴尬局面吗?会是人工翻译的终结者吗? 世界这么大,我想去看看!十一长假临近,梦想中的你背起行囊,自由行走在异国的大街小巷.然而现实的画 ...

  8. 03-从零玩转JavaWeb-创建类与对象

    创建类创建对象 一.什么是成员变量 对象的一些状态特征使用成员变量表示   二.行为什么什么表示 对象的 行为 使用 方法 表示   三.如何创建对象 class 类名{0 ~ N个成员变量0 ~ N ...

  9. ABAP开源项目清单

    因为曾经的“SAP Code Exchange”平台已经于2013年倒闭,现在无论在SCN还是网络上都比较难找到一个地方来关注全部的优秀ABAP开源项目. 本文将这些项目的地址和他们的描述列出,以供参 ...

  10. Leetcode题解(八)

    26.Remove Duplicates from Sorted Array 题目 直接上代码,方法很简单: class Solution { public: int removeDuplicates ...