《java.util.concurrent 包源码阅读》15 线程池系列之ScheduledThreadPoolExecutor 第二部分
这篇文章主要说说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 第二部分的更多相关文章
- 《java.util.concurrent 包源码阅读》 结束语
<java.util.concurrent 包源码阅读>系列文章已经全部写完了.开始的几篇文章是根据自己的读书笔记整理出来的(当时只阅读了部分的源代码),后面的大部分都是一边读源代码,一边 ...
- 《java.util.concurrent 包源码阅读》13 线程池系列之ThreadPoolExecutor 第三部分
这一部分来说说线程池如何进行状态控制,即线程池的开启和关闭. 先来说说线程池的开启,这部分来看ThreadPoolExecutor构造方法: public ThreadPoolExecutor(int ...
- 《java.util.concurrent 包源码阅读》04 ConcurrentMap
Java集合框架中的Map类型的数据结构是非线程安全,在多线程环境中使用时需要手动进行线程同步.因此在java.util.concurrent包中提供了一个线程安全版本的Map类型数据结构:Concu ...
- 《java.util.concurrent 包源码阅读》02 关于java.util.concurrent.atomic包
Aomic数据类型有四种类型:AomicBoolean, AomicInteger, AomicLong, 和AomicReferrence(针对Object的)以及它们的数组类型, 还有一个特殊的A ...
- 《java.util.concurrent 包源码阅读》17 信号量 Semaphore
学过操作系统的朋友都知道信号量,在java.util.concurrent包中也有一个关于信号量的实现:Semaphore. 从代码实现的角度来说,信号量与锁很类似,可以看成是一个有限的共享锁,即只能 ...
- 《java.util.concurrent 包源码阅读》06 ArrayBlockingQueue
对于BlockingQueue的具体实现,主要关注的有两点:线程安全的实现和阻塞操作的实现.所以分析ArrayBlockingQueue也是基于这两点. 对于线程安全来说,所有的添加元素的方法和拿走元 ...
- 《java.util.concurrent 包源码阅读》09 线程池系列之介绍篇
concurrent包中Executor接口的主要类的关系图如下: Executor接口非常单一,就是执行一个Runnable的命令. public interface Executor { void ...
- 《java.util.concurrent 包源码阅读》24 Fork/Join框架之Work-Stealing
仔细看了Doug Lea的那篇文章:A Java Fork/Join Framework 中关于Work-Stealing的部分,下面列出该算法的要点(基本是原文的翻译): 1. 每个Worker线程 ...
- 《java.util.concurrent 包源码阅读》25 Fork/Join框架之Fork与Work-Stealing(重写23,24)
在写前面两篇文章23和24的时候自己有很多细节搞得不是很明白,这篇文章把Fork和Work-Stealing相关的源代码重新梳理一下. 首先来看一些线程池定义的成员变量: 关于scanGuard: v ...
- 《java.util.concurrent 包源码阅读》22 Fork/Join框架的初体验
JDK7引入了Fork/Join框架,所谓Fork/Join框架,个人解释:Fork分解任务成独立的子任务,用多线程去执行这些子任务,Join合并子任务的结果.这样就能使用多线程的方式来执行一个任务. ...
随机推荐
- 关于extjs表单布局的几种方式
一.用column布局 layout:'column', defaults:{ style:'float:left;margin:4px;', columnWidth: 0.49, msgTarget ...
- bind、apply与call
bind.apply与call 先说观点:不论是bind.apply还是call,最大的好处就是代码复用. bind 在开发中,我们只有复用代码时,才会出现this指向需要改动的情况. 纵观bind的 ...
- 真正从0开始用Unity3D制作类战地2玩法的类龙之谷、王者荣耀的手游(暨全平台游戏)
如题,(从2017年10月18日开始)正在利用业余时间研发一款神泣Shaiya2手游,引擎用Unity3D. 原因主要有2点: 对神泣太多感情,希望能做点什么来纪念乃至留下神泣这款网游: 时机已到,是 ...
- 【网络爬虫入门05】分布式文件存储数据库MongoDB的基本操作与爬虫应用
[网络爬虫入门05]分布式文件存储数据库MongoDB的基本操作与爬虫应用 广东职业技术学院 欧浩源 1.引言 网络爬虫往往需要将大量的数据存储到数据库中,常用的有MySQL.MongoDB和Red ...
- UVa12100,Printer Queue
水题,1A过的 数据才100,o(n^3)都能过,感觉用优先队列来做挺麻烦的,直接暴力就可以了,模拟的队列,没用stl #include <iostream> #include <c ...
- struts2使用模型传值
用户bean package userBeans; public class User { private String username; public String getUsername() { ...
- Python学习常用的好网站
以下总结出自己在学习python期间常用的网址或者资源,其中包括很多人的博客,方便自己从这个入口查找资源. 1.https://www.liaoxuefeng.com/wiki/00143160895 ...
- C GOTO使用示例
GOTO虽然会破坏程序的结构,使用代码可读性变差,但是GOTO依然还是有可用的地方 #include <stdio.h>#include <stdbool.h> int mai ...
- JAVA基础知识总结:九
二.面向对象特性之继承 1.什么是继承? 如果两个或者两个以上的类具有相同的属性和方法,我们可以抽取一个类出来,在抽取出来的类中声明各个类中公共的部分 被抽取出来的类-------父类,基类,超类 两 ...
- LeetCode 152. Maximum Product Subarray (最大乘积子数组)
Find the contiguous subarray within an array (containing at least one number) which has the largest ...