DelayQueue的使用及源码分析
首先感谢这位博客的作者:https://blog.csdn.net/dkfajsldfsdfsd/article/details/88966814让我理解了DelayQueue中Leader-Followr模式的使用。
基本原理
DelayQueue是一个没有边界BlockingQueue实现,加入其中的元素必需实现Delayed接口。当生产者线程调用put之类的方法加入元素时,会触发Delayed接口中的compareTo方法进行排序,也就是说队列中元素的顺序是按到期时间排序的,而非它们进入队列的顺序。排在队列头部的元素是最早到期的,越往后到期时间赿晚。
消费者线程查看队列头部的元素,注意是查看不是取出。然后调用元素的getDelay方法,如果此方法返回的值小0或者等于0,则消费者线程会从队列中取出此元素,并进行处理。如果getDelay方法返回的值大于0,则消费者线程wait返回的时间值后,再从队列头部取出元素,此时元素应该已经到期。
DelayQueue的Leader-Followr
DelayQueue是Leader-Followr模式的变种,消费者线程处于等待状态时,总是等待最先到期的元素,而不是长时间的等待。消费者线程尽量把时间花在处理任务上,最小化空等的时间,以提高线程的利用效率。
以下通过队列及消费者线程状态变化大致说明一下DelayQueue的运行过程。
-
初始状态
因为队列是没有边界的,向队列中添加元素的线程不会阻塞,添加操作相对简单,所以此图不考虑向队列添加元素的生产者线程。假设现在共有三个消费者线程。
队列中的元素按到期时间排序,队列头部的元素2s以后到期。消费者线程1查看了头部元素以后,发现还需要2s才到期,于是它进入等待状态,2s以后醒来,等待头部元素到期的线程称为Leader线程。
消费者线程2与消费者线程3处于待命状态,它们不等待队列中的非头部元素。当消费者线程1拿到对象5以后,会向它们发送signal。这个时候两个中的一个会结束待命状态而进入等待状态。
-
2S以后
消费者线程1已经拿到了对象5,从等待状态进入处理状态,处理它取到的对象5,同时向消费者线程2与消费者线程3发送signal。
消费者线程2与消费者线程3会争抢领导权,这里是消费者线程2进入等待状态,成为Leader线程,等待2s以后对象4到期。而消费者线程3则继续处于待命状态。
此时队列中加入了一个新元素对象6,它10s后到期,排在队尾。
-
又2S以后
先看线程1,如果它已经结束了对象5的处理,则进入待命状态。如果还没有结束,则它继续处理对象5。
消费线程2取到对象4以后,也进入处理状态,同时给处于待命状态的消费线程3发送信号,消费线程3进入等待状态,成为新的Leader。现在头部元素是新插入的对象7,因为它1s以后就过期,要早于其它所有元素,所以排到了队列头部。
-
又1S后
一种不好的结果:
消费线程3一定正在处理对象7。消费线程1与消费线程2还没有处理完它们各自取得的对象,无法进入待命状态,也更加进入不了等待状态。此时对象3马上要到期,那么如果它到期时没有消费者线程空下来,则它的处理一定会延期。
可以想见,如果元素进入队列的速度很快,元素之间的到期时间相对集中,而处理每个到期元素的速度又比较慢的话,则队列会越来越大,队列后边的元素延期处理的时间会越来越长。
另外一种好的结果:
消费线程1与消费线程2很快的完成对取出对象的处理,及时返回重新等待队列中的到期元素。一个处于等待状态(Leader),对象3一到期就立刻处理。另一个则处于待命状态。这样,每一个对象都能在到期时被及时处理,不会发生明显的延期。
所以,消费者线程的数量要够,处理任务的速度要快。否则,队列中的到期元素无法被及时取出并处理,造成任务延期、队列元素堆积等情况。
例子
package com.strawberry.queueExample;
import java.util.Random;
import java.util.concurrent.*;
public class DelayQueueExample {
public static void main(String[] args) throws InterruptedException {
//延迟任务队列
DelayQueue<DelayQueueTask> delayQueue = new DelayQueue<>();
for(int i = 0; i < 10;i++){
//创建延迟任务,启动时间在1~2s之间
DelayQueueTask delayQueueProducer = new DelayQueueTask("work" + i);
delayQueue.add(delayQueueProducer);
}
//启动消费线程去消费延迟任务
for (int i = 0; i < 1; i++) {
new DelayConsumer(delayQueue).start();
}
}
static class DelayQueueTask implements Delayed {
private static long currentTime = System.currentTimeMillis();
//任务启动时间
private long scheduleTime;
//任务名称
private String taskName;
Random random = new Random();
public DelayQueueTask(String taskName) {
int randomInteger = random.nextInt(1000);
currentTime += 1000 + randomInteger;
this.scheduleTime = currentTime;
this.taskName = taskName;
}
public long getScheduleTime() {
return scheduleTime;
}
public void setScheduleTime(long scheduleTime) {
this.scheduleTime = scheduleTime;
}
public String getTaskName() {
return taskName;
}
public void setTaskName(String taskName) {
this.taskName = taskName;
}
@Override
public long getDelay(TimeUnit unit) {
//用于判断是否可以取出,小于等于0时可以取出
long delay = this.scheduleTime - System.currentTimeMillis();
return unit.convert(delay,TimeUnit.MICROSECONDS);
}
@Override
public int compareTo(Delayed o) {
//插入队列中进行排序
return (int) (this.scheduleTime - ((DelayQueueTask)o).scheduleTime);
}
//模拟处理任务
public void handleJob() throws InterruptedException {
System.out.println(this.taskName + "开始工作,延迟时间:" + (System.currentTimeMillis() - this.scheduleTime));
//处理任务时间为2s
Thread.sleep(2000);
}
}
static class DelayConsumer extends Thread{
private DelayQueue<DelayQueueTask> delayQueue;
public DelayConsumer(DelayQueue<DelayQueueTask> delayQueue) {
this.delayQueue = delayQueue;
}
@Override
public void run() {
while (true){
DelayQueueTask take = null;
try {
take = delayQueue.take();
take.handleJob();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
当启动1个线程去消费时,必然出现延迟的现象,因为每个任务的处理时间是2s,任务的启动时间在1~2s之间,运行结果如下:
work0开始工作,延迟时间:0
work1开始工作,延迟时间:288
work2开始工作,延迟时间:633
work3开始工作,延迟时间:1610
work4开始工作,延迟时间:1623
work5开始工作,延迟时间:2095
work6开始工作,延迟时间:2184
work7开始工作,延迟时间:2543
work8开始工作,延迟时间:2559
work9开始工作,延迟时间:3202
队列中的值越多,越后面延迟越来越大
当启动2个线程去消费时,延迟现在会有很大的改善,运行结果如下:
work0开始工作,延迟时间:0
work1开始工作,延迟时间:0
work2开始工作,延迟时间:0
work3开始工作,延迟时间:0
work4开始工作,延迟时间:0
work5开始工作,延迟时间:0
work6开始工作,延迟时间:0
work7开始工作,延迟时间:0
work8开始工作,延迟时间:0
work9开始工作,延迟时间:0
延迟都为0
当启动3个线程去消费时,结果如下:
work0开始工作,延迟时间:0
work1开始工作,延迟时间:0
work2开始工作,延迟时间:0
work3开始工作,延迟时间:0
work4开始工作,延迟时间:0
work5开始工作,延迟时间:0
work6开始工作,延迟时间:0
work7开始工作,延迟时间:0
work8开始工作,延迟时间:0
work9开始工作,延迟时间:0
所以开3个线程,就会出现资源浪费。
最优的消费者线程的个数与任务启动的时间间隔好像存在这样的关系:单个任务处理时间的最大值 / 相邻任务的启动时间最小间隔 = 最优线程数,如果最优线程数是小数,则取整数后加1,比如1.3的话,那么最优线程数应该是2。
本例中,单个任务处理时间的最大值固定为2s。
相邻任务的启动时间最小间隔为1s。
则消费者线程数为2/1=2。
如果消费者线程数小于此值,则来不及处理到期的任务。如果大于此值,线程太多,在调度、同步上花更多的时间,无益改善性能。
源码解析
//全局锁
private final transient ReentrantLock lock = new ReentrantLock();
//保存数据的队列,拥有优先级功能
private final PriorityQueue<E> q = new PriorityQueue<E>();
//头线程,负责取头部元素和唤醒等待中的元素
private Thread leader = null;
//唤醒的条件
private final Condition available = lock.newCondition();
新增:
public boolean add(E e) {
return offer(e);
}
public boolean offer(E e) {
final ReentrantLock lock = this.lock;
//锁住
lock.lock();
try {
//添加元素
q.offer(e);
//如果添加的元素是头部元素
if (q.peek() == e) {
//头部线程致为空,让后面等待中的线程可以取到(为何设置为空,查看take方法中的逻辑)
leader = null;
//唤醒等待中的线程
available.signal();
}
return true;
} finally {
//释放锁
lock.unlock();
}
}
public void put(E e) {
offer(e);
}
public boolean offer(E e, long timeout, TimeUnit unit) {
return offer(e);
}
取首个元素,不阻塞
//获取首个元素但不弹出元素
public E peek() {
final ReentrantLock lock = this.lock;
//锁住
lock.lock();
try {
return q.peek();
} finally {
//释放锁
lock.unlock();
}
}
//获取首个元素并弹出元素
public E poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
E first = q.peek();
//到期时间没有到,返回null
if (first == null || first.getDelay(NANOSECONDS) > 0)
return null;
else
return q.poll();
} finally {
lock.unlock();
}
}
取首个元素,阻塞
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock;
//锁住,遇到中段抛出异常
lock.lockInterruptibly();
try {
for (;;) {
//获取第一个元素
E first = q.peek();
//第一个元素为空,线程阻塞
if (first == null)
available.await();
else {
//不为空,获取到期时间
long delay = first.getDelay(NANOSECONDS);
//到期时间小于等于0,可以取出,取出即可
if (delay <= 0)
return q.poll();
//到期时间大于0,不能取出
first = null; // don't retain ref while waiting
//leader线程不为空,说明有线程在等待头部元素了,此线程阻塞即可
if (leader != null)
available.await();
else {
//leader线程为空,说明没有线程在等待头部元素,将当前线程设置为头部线程,进行等待
Thread thisThread = Thread.currentThread();
leader = thisThread;
try {
//等待delay时间
available.awaitNanos(delay);
} finally {
//执行到此处,说明当前线程等待的头部元素已经被取出
//此时leader线程等于当前线程,需要把leader线程致为null,方便其他线程获取leader线程去获取头部元素
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
//如果leader=null并且队列中有值,说明没有线程在等待取出头部元素,需要唤醒其他线程去成为leader线程去获取值
if (leader == null && q.peek() != null)
available.signal();
//释放锁
lock.unlock();
}
}
//阻塞传入的时间
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
//得到阻塞的时间
long nanos = unit.toNanos(timeout);
final ReentrantLock lock = this.lock;
//锁住,遇到中段抛出异常
lock.lockInterruptibly();
try {
for (;;) {
//获取第一个元素
E first = q.peek();
//第一个元素为空
if (first == null) {
//如果阻塞的时间小于等于0了,返回空
if (nanos <= 0)
return null;
else
//阻塞nanos时间
nanos = available.awaitNanos(nanos);
} else {
//不为空,获取到期时间
long delay = first.getDelay(NANOSECONDS);
//到期时间小于等于0,可以取出,取出即可
if (delay <= 0)
return q.poll();
//阻塞时间小于0,返回null
if (nanos <= 0)
return null;
first = null; // don't retain ref while waiting
//到期时间大于超时时间或者leader线程不为null
if (nanos < delay || leader != null)
//线程睡眠
nanos = available.awaitNanos(nanos);
else {
//leader线程为空,说明没有线程在等待头部元素,将当前线程设置为头部线程,进行等待
Thread thisThread = Thread.currentThread();
leader = thisThread;
try {
//线程睡眠delay时间
long timeLeft = available.awaitNanos(delay);
//计算到期时间
nanos -= delay - timeLeft;
} finally {
//执行到此处,说明当前线程等待的头部元素已经被取出
//此时leader线程等于当前线程,需要把leader线程致为null,方便其他线程获取leader线程去获取头部元素
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
//如果leader=null并且队列中有值,说明没有线程在等待取出头部元素,需要唤醒其他线程去成为leader线程去获取值
if (leader == null && q.peek() != null)
available.signal();
//释放锁
lock.unlock();
}
}
//执行到此处,说明当前线程等待的头部元素已经被取出
//此时leader线程等于当前线程,需要把leader线程致为null,方便其他线程获取leader线程去获取头部元素
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
//如果leader=null并且队列中有值,说明没有线程在等待取出头部元素,需要唤醒其他线程去成为leader线程去获取值
if (leader == null && q.peek() != null)
available.signal();
//释放锁
lock.unlock();
}
}