在【Java并发学习一】如何实现一个线程池上进行扩展,我们想一下如何实现一个定时任务线程池呢?
所谓 “定时任务线程池” 就是指放入线程池的任务,可以按照指定的等待周期循环执行。
Java里面ScheduledThreadPoolExecutor
这个类实现了这种功能。Spring里面的定时任务也是在ScheduledThreadPoolExecutor
的基础上扩展而来。
如何实现
我们先回顾下【Java并发学习一】如何实现一个线程池中实现的自定义线程池:
如图,放入线程池的任务,在线程数超过corePoolSize
的情况下会放入队列,而线程池内的线程则不断从队列中读取任务消费。
如果我们想要一个放入的任务每隔一段时间(如一小时)定时执行,似乎挺简单:
- 消费完的任务,需要再放进队列中被消费,
- 线程池中线程取任务的时间不能是马上,得等待一小时后才消费。
第一点不难,关键是第二点如何实现。思考下,应该不能从线程下手,因为每个任务定时时间是不同的,线程消费时是不好控制的。那就只有从队列下手了。
我们将放入的任务增加一个delay
延迟字段,然后使它被取出时,等待delay
这么长就行。
OK,看到这里,你已经把定时任务线程池的原理理解的差不多啦~ 接下来我们看具体实现细节。
实现细节
1. 延迟队列的实现
最难的地方也就是 延迟队列 的实现。我们借鉴下已有的实现,在Java里面查找了下已有队列,果然发现一个DelayQueue
的类。研究一番后发现,延迟队列需要用到一种 叫做 “堆” 的数据结构。
堆
堆其实就是一个完全二叉树,延迟队列中用的是 最小堆(父结点<=子结点)。一般用数组来存储,i
结点的父结点下标就为(i – 1) / 2
。它的左右子结点下标分别为2i + 1
和2i + 2
。
- 在堆数组尾部添加元素时,需要不停将该元素与其父结点进行对比交换,类似于元素在“上升”。也就是使新添加的元素插入一个有序的序列中,形成一个新的有序堆序列
- 堆删除元素时,总是先删除根结点,然后将最后一个元素移到根结点,与子结点对比交换,类似于元素在“下沉”。最终形成新的有序堆序列。
关于堆的更多细节可以自行百度谷歌,或者查看我之前排序算法总结里面的堆排序: 数据结构基础(六)排序
自定义延迟队列
为什么延迟队列用堆实现呢?
因为堆中元素是有顺序的,延迟队列中排序是以 任务的等待周期
比较,这样保证了从延迟队列中取出元素(即堆中获取头节点),总是取出的等待周期最小的任务消费。
/**
* 定义任务的compareTo方法,用于堆插入或者删除时的堆排序
*/
@Override
public int compareTo(Delayed o) {
MyScheduledFutureTask x = (MyScheduledFutureTask)o;
long diff = time - x.time;
if (diff < 0)
return -1;
else if (diff > 0)
return 1;
else
return 1;
}
那延迟队列的延迟如何实现呢?
延迟队列用Condition.awaitNanos(delay)
条件变量来实现了线程的等待。我们看下延迟队列的成员变量,queue
就是一个堆结构的任务数组;lock
锁保证的队列新增和删除时的线程安全;lock
、Condition
和leader
(领导线程) 一起,控制线程的等待和唤醒。具体细节看下面。
/**
* 自定义延迟队列
*/
public class MyDelayQueue extends AbstractQueue implements BlockingQueue{
/** 堆数据结构 */
private MyScheduledFutureTask[] queue = new MyScheduledFutureTask[16];
/** 队列元素个数 */
private int size = 0;
/** 锁,用于队列新增和删除时保持线程安全 */
private final transient ReentrantLock lock = new ReentrantLock();
/** 用于实现队列延迟取出元素 */
private final Condition available = lock.newCondition();
/**
* 领导线程,可理解为正在获取节点的线程
* 和锁、Condition一起,
* 控制队列延迟获取节点时,线程的等待和唤醒
*/
private Thread leader = null;
………………
}
延迟队列中添加任务是怎样的?
延迟队列中添加任务很简单,直接往堆尾部增加节点,然后执行 “上升”操作来重排序即可
public boolean add(Object o) {
lock.lock();
try {
//队列空间不足时,扩展
if(size >= queue.length-1){
queue = Arrays.copyOf(queue, queue.length*2);
}
MyScheduledFutureTask task = (MyScheduledFutureTask) o;
//queue没有任务时,直接往数组第一个放入任务
if(size == 0){
queue[size++] = task;
}
//queue已经有任务时,在堆尾部增加任务,并实行堆上浮操作
else {
size++;
siftUp(size-1, task);
}
} finally {
lock.unlock();
}
return true;
}
重点!!!:延迟队列中取出任务是怎样的?
延迟队列中获取并删除任务比较复杂,因为线程池中多个线程同时在从延迟队列中取任务,所以需要用lock
、Condition
和leader
(领导线程) 一起,控制当一个线程在取任务,其余线程阻塞,等到该任务获取完毕,再唤醒其余线程。
重点是leader
的理解,leader
可理解为正在获取节点的线程。当leader
为空时,证明没有线程在从队列中获取节点,该线程可自己成为leader
获取任务节点;当leader
不为空时,证明有线程正在获取节点,此时的leader
在堵塞倒计时中(awaitNanos(delay)
),故该线程需要阻塞;当线程取元素结束时,都需要唤醒Condition
等待上的任一线程。
public MyScheduledFutureTask take() throws InterruptedException {
lock.lockInterruptibly();
try {
for (;;) {
//取出堆中的头节点
MyScheduledFutureTask first = queue[0];
//如果堆中没有节点,则挂起线程
if (first == null)
available.await();
else {
//获取节点任务的等待时间
long delay = first.getDelay(NANOSECONDS);
//如果已经不需要等待,直接返回节点任务,并将堆中尾节点视为头节点进行堆下沉排序
if (delay <= 0)
return finishPoll(first);
//为下面代码线程等待时不再持有无用的first对象,直接释放它
first = null;
//leader不为空,则某个awaitNanos线程已经在取任务,挂起线程
if (leader != null)
available.await();
//leader为空,此时没有线程在取任务
else {
//设置leader为当前线程
Thread thisThread = Thread.currentThread();
leader = thisThread;
//调用awaitNanos方法等待固定时间后,等待其他线程的唤醒
try {
available.awaitNanos(delay);
} finally {
//leader线程被唤醒,下个循环将返回节点,此时将leader设置为null
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
//队列非空时,unlock前 随机唤醒等待条件上的任一队列
if (queue[0] != null)
available.signal();
lock.unlock();
}
}
/**
* 删除f节点,将堆中尾节点设置为头节点,然后进行下沉排序
*/
private MyScheduledFutureTask finishPoll(MyScheduledFutureTask f) {
int s = --size;
MyScheduledFutureTask x = queue[s];
queue[s] = null;
if (s != 0)
siftDown(0, x);
return f;
}
最后贴出完整的自定义延迟队列代码:
/**
* 自定义延迟队列
*/
public class MyDelayQueue extends AbstractQueue implements BlockingQueue{
/** 堆数据结构 */
private MyScheduledFutureTask[] queue = new MyScheduledFutureTask[16];
/** 队列元素个数 */
private int size = 0;
/** 锁,用于队列新增和删除时保持线程安全 */
private final transient ReentrantLock lock = new ReentrantLock();
/** 用于实现队列延迟取出元素 */
private final Condition available = lock.newCondition();
/**
* 领导线程,可理解为正在获取节点的线程
* 和锁、Condition一起,
* 控制队列延迟获取节点时,线程的等待和唤醒
*/
private Thread leader = null;
@Override
public int size() {
return size;
}
@Override
public boolean add(Object o) {
lock.lock();
try {
//队列空间不足时,扩展
if(size >= queue.length-1){
queue = Arrays.copyOf(queue, queue.length*2);
}
MyScheduledFutureTask task = (MyScheduledFutureTask) o;
//queue没有任务时,直接往数组第一个放入任务
if(size == 0){
queue[size++] = task;
}
//queue已经有任务时,在堆尾部增加任务,并实行堆上浮操作
else {
size++;
siftUp(size-1, task);
}
} finally {
lock.unlock();
}
return true;
}
@Override
public MyScheduledFutureTask take() throws InterruptedException {
lock.lockInterruptibly();
try {
for (;;) {
//取出堆中的头节点
MyScheduledFutureTask first = queue[0];
//如果堆中没有节点,则挂起线程
if (first == null)
available.await();
else {
//获取节点任务的等待时间
long delay = first.getDelay(NANOSECONDS);
//如果已经不需要等待,直接返回节点任务,并将下一个节点视为头节点进行堆排序
if (delay <= 0)
return finishPoll(first);
//下面代码线程等待时不再持有无用的first对象,直接释放它
first = null;
//leader不为空,则某个awaitNanos线程已经在取任务,挂起线程
if (leader != null)
available.await();
//leader为空,此时没有线程在取任务
else {
//设置leader为当前线程
Thread thisThread = Thread.currentThread();
leader = thisThread;
//调用awaitNanos方法等待固定时间后,将被唤醒
try {
available.awaitNanos(delay);
} finally {
//任务等待完毕,leader线程被唤醒,下个循环将返回节点,此时将leader设置为null
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
//队列非空时,unlock前 随机唤醒等待条件上的任一队列
if (queue[0] != null)
available.signal();
lock.unlock();
}
}
/**
* 删除f节点,将堆中尾节点设置为头节点,然后进行下沉排序
*/
private MyScheduledFutureTask finishPoll(MyScheduledFutureTask f) {
int s = --size;
MyScheduledFutureTask x = queue[s];
queue[s] = null;
if (s != 0)
siftDown(0, x);
return f;
}
/**
* 在堆尾部增加节点,实行堆排序的上浮操作
*/
private void siftUp(int k, MyScheduledFutureTask key) {
//如果子节点比父节点大,则替换
while (k > 0) {
int parent = (k - 1) / 2;
MyScheduledFutureTask e = queue[parent];
if (key.compareTo(e) >= 0)
break;
queue[k] = e;
k = parent;
}
queue[k] = key;
}
/**
* 从堆的顶部拿取节点,实现堆排序的下沉操作
*/
private void siftDown(int k, MyScheduledFutureTask key) {
int half = size / 2;
while (k < half) {
int child = (k*2) + 1;
MyScheduledFutureTask c = queue[child];
int right = child + 1;
if (right < size && c.compareTo(queue[right]) > 0)
c = queue[child = right];
if (key.compareTo(c) <= 0)
break;
queue[k] = c;
k = child;
}
queue[k] = key;
}
//…………其余方法略
}
2. 线程任务的实现
Runnable
的包装类,记录每个任务执行时的等待周期period
和下个周期任务应该触发的时间time
,以及run()
结束前,将任务再次放入队列中
/**
* 定时任务执行类
*/
public class MyScheduledFutureTask
implements Runnable, Delayed {
/** 任务触发时间的纳秒值 */
private long time;
/** 循环间隔的纳秒值 */
private final long period;
/** 线程池中的队列 */
private BlockingQueue queue;
/** 执行任务 */
private Runnable task;
public MyScheduledFutureTask(Runnable r, long time, int period, BlockingQueue<Runnable> queue) {
this.task = r;
this.time = time;
this.period = period;
this.queue = queue;
}
/**
* 自定义任务队列实现了堆数据结构,此方法用于堆插入或者删除时的堆排序
*/
@Override
public int compareTo(Delayed o) {
MyScheduledFutureTask x = (MyScheduledFutureTask)o;
long diff = time - x.time;
if (diff < 0)
return -1;
else if (diff > 0)
return 1;
else
return 1;
}
/**
* 获取触发时间与当前时间的时间差
*/
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(time - System.nanoTime(), NANOSECONDS);
}
@Override
public void run() {
//执行逻辑
task.run();
//任务执行结束后,将下次任务触发的时间增加一周期
time += TimeUnit.SECONDS.toNanos((long)period);
//重新往线程池队列中加入此任务
queue.add(this);
}
}
3. 定时线程池的实现
有了上面的基础,定时线程池的实现就很简单了。scheduleAtFixedRate()
方法中,将执行的任务command
封装为包装类MyScheduledFutureTask
;然后放入延迟队列中;最后调用ensurePrestart()
方法往线程池放入一个空任务,使线程池创建线程开始不断读取队列中的任务执行。
/**
* 自定义简单定时线程池
*/
public class MyScheduledThreadPool extends MyThreadPool{
public MyScheduledThreadPool(int initPoolNum) {
super(initPoolNum, Integer.MAX_VALUE, new MyDelayQueue());
}
/**
* 每隔固定时间周期执行任务
* @param command 任务
* @param period 时间周期(以秒为单位)
*/
public void scheduleAtFixedRate(Runnable command, int period) {
if (command == null)
throw new NullPointerException();
if (period <= 0)
throw new IllegalArgumentException();
//包装任务为周期任务
MyScheduledFutureTask mScheduledTask =
new MyScheduledFutureTask(command, triggerTime(period), period, getTaskQueue());
//延迟周期执行
delayedExecute(mScheduledTask);
}
private void delayedExecute(MyScheduledFutureTask task) {
getTaskQueue().add(task);
ensurePrestart();
}
/**
* 确保线程池已经启动,有线程会去读取队列,并执行任务
*/
void ensurePrestart() {
execute(null);
}
/**
* 获取触发时间
* @param period 延迟时间
* @return 返回触发时间
*/
long triggerTime(int period) {
return System.nanoTime() + TimeUnit.SECONDS.toNanos((long)period);
}
}
/**
* 自定义简单线程池
*/
public class MyThreadPool{
/**存放线程的集合*/
private ArrayList<MyWorkThread> threads;
/**任务队列*/
private BlockingQueue<Runnable> taskQueue;
/**线程池初始限定大小*/
private int threadNum;
/** 线程池最大大小 */
private int maxThreadNum;
/**已经工作的线程数目*/
private int workThreadNum;
private final ReentrantLock mainLock = new ReentrantLock();
public MyThreadPool(int initPoolNum) {
this.threadNum = initPoolNum;
maxThreadNum = initPoolNum;
this.threads = new ArrayList<>(initPoolNum);
//任务队列初始化为线程池线程数的四倍
this.taskQueue = new ArrayBlockingQueue<>(initPoolNum*4);
this.workThreadNum = 0;
}
public MyThreadPool(int initPoolNum, int maxThreadNum, BlockingQueue<Runnable> taskQueue) {
this.threadNum = initPoolNum;
this.maxThreadNum = maxThreadNum;
this.threads = new ArrayList<>(initPoolNum);
//任务队列初始化为线程池线程数的四倍
this.taskQueue = taskQueue;
this.workThreadNum = 0;
}
public void execute(Runnable runnable) {
try {
mainLock.lock();
//线程池未满,每加入一个任务则开启一个线程
if(workThreadNum < threadNum) {
MyWorkThread myThead = new MyWorkThread(runnable);
myThead.start();
threads.add(myThead);
workThreadNum++;
}
//线程池已满,放入任务队列,等待有空闲线程时执行
else {
//队列已满,无法添加、且线程数小于最大线程数时,新增一个任务线程跑任务
if(!taskQueue.offer(runnable) && workThreadNum < maxThreadNum) {
MyWorkThread overLimitThead = new MyWorkThread(runnable);
overLimitThead.start();
threads.add(overLimitThead);
workThreadNum++;
}
//队列已满,无法添加、且线程数大于等于最大线程数时,拒绝任务
else{
rejectTask();
}
}
} finally {
mainLock.unlock();
}
}
public BlockingQueue<Runnable> getTaskQueue() {
return taskQueue;
}
private void rejectTask() {
System.out.println("任务队列已满,无法继续添加,请扩大您的初始化线程池!");
}
public static void main(String[] args) {
MyThreadPool myThreadPool = new MyThreadPool(5);
Runnable task = new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread().getName()+"执行中");
}
};
for (int i = 0; i < 20; i++) {
myThreadPool.execute(task);
}
}
/**
* 自定义线程类
*/
class MyWorkThread extends Thread{
private Runnable task;
public MyWorkThread(Runnable runnable) {
this.task = runnable;
}
@Override
public void run() {
//该线程一直启动着,不断从任务队列取出任务执行
while (true) {
//如果初始化任务不为空,则执行初始化任务
if(task != null) {
task.run();
task = null;
}
//否则去任务队列取任务并执行
else {
Runnable queueTask = null;
try {
queueTask = taskQueue.take();
queueTask.run();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
补充:Java中定时线程池的实现
看完上面的内容,你其实已经对Java中ScheduledThreadPoolExecutor
的实现原理了解的差不多了。
这里只补充下ScheduledThreadPoolExecutor
的定时执行任务的四种方法差别:
public ScheduledFuture<?> schedule(Runnable command,long delay,TimeUnit unit) {…………}
public <V> ScheduledFuture<V> schedule(Callable<V> callable,long delay,TimeUnit unit) {…………}
public ScheduledFuture<?> scheduleAtFixedRate(Runnable command,long initialDelay,long period,TimeUnit unit) {…………}
public ScheduledFuture<?> scheduleWithFixedDelay(Runnable command,long initialDelay,long delay,TimeUnit unit{…………}
前两个schedule方法是延迟执行任务,且只执行一次不循环。它们的区别在于形参中的任务类型不同,一个是Runnable
,一个是Callable
,其实Runnable
最后也被包装成了Callable
类型。
只执行一次的实现是:将任务(ScheduledFutureTask
)的成员变量period
设置为0,当period
为0时任务运行结束不再往队列中重新加入。
后两个方法是周期执行任务,initialDelay
形参指明任务第一次执行时的延迟时间。它们的差别在于,scheduleAtFixedRate
是严格按照周期period
执行任务,例如任务每隔四秒执行一次。scheduleWithFixedDelay
是从任务结束起开始计时执行任务,例如任务运行完成后,再隔四秒执行一次。
它们的实现原理是:将任务(ScheduledFutureTask
)的成员变量period
设置为正数时,代表fixed-rate
方式执行;设置为负数时,代表fixed-delay
方式执行任务,。具体体现在:方法setNextRunTime()
(设置下一次任务执行时间的)中,根据period
的不同,计算方式不同:
private void setNextRunTime() {
long p = period;
//fixed-rate方式,直接在上一次执行任务的time上加上周期
if (p > 0)
time += p;
//fixed-delay方式,在现在时间now()上加上周期
else
time = triggerTime(-p);
}
long triggerTime(long delay) {
return now() +
((delay < (Long.MAX_VALUE >> 1)) ? delay : overflowFree(delay));
}