DelayQueue
一、类结构
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z3upEzpL-1585578489822)(C:\Users\MI\AppData\Roaming\Typora\typora-user-images\1585324001471.png)]
-
从继承关系看,实现了BlockingQueue接口,因此是一个阻塞队列;
-
实现了Delayed接口,因此是一个延迟队列,其在指定时间才能获取队列元素,队列头元素是最接近过期的元素。没有过期元素的话,使用poll()方法会返回null值,超时判定是通过getDelay(TimeUnit.NANOSECONDS)方法的返回值小于等于0来判断。延时队列不能存放空元素。
-
该延时队列实现了Iterator接口,但遍历顺序不保证是元素的实际存放顺序。
Delayed接口
public interface Delayed extends Comparable<Delayed> {
/**
* 返回剩余到期时间
*/
long getDelay(TimeUnit unit);
}
二、类属性
//使用可重入锁
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 DelayQueue() {}
//指定初始集合元素
public DelayQueue(Collection<? extends E> c) {
this.addAll(c);
}
四、入队操作
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) {//堆顶元素是e时,即入队成功
leader = null;//等待的线程置为null
available.signal();//唤醒等待的队列
}
//返回true
return true;
} finally {
//释放锁
lock.unlock();
}
}
流程:
- 加锁
- 入队
- 如果优先级队列堆顶是添加的新元素,说明入队成功,置排队线程为空,并唤醒在available队列中等待的线程
- 释放锁
其他几个入队都是直接调用offer()方法
五、出队
5.1 poll
public E poll() {
final ReentrantLock lock = this.lock;
lock.lock();//上锁
try {
E first = q.peek();//获取堆顶元素(未出队)
if (first == null || //堆顶为空,即队列为空,直接返回null
first.getDelay(NANOSECONDS) > 0)//没到期就返回null
return null;
else
//到期了就返回第一个元素
return q.poll();
} finally {
//释放锁
lock.unlock();
}
}
- 上锁
- 查看堆顶元素,如果为空或还未到期限,则返回null
- 如果到期了则堆顶元素出队并返回
- 释放锁
因此poll方法是不阻塞的,堆顶元素为空或没到期限时直接返回null
5.2 take
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);//剩余延迟时间
if (delay <= 0)//小于0,说明到时间了
return q.poll();//出队返回
first = null; //还没到期,则置为null
if (leader != null)//等待线程非空,说明有线程在等待
available.await();//阻塞等待
else {//没有线程等待
Thread thisThread = Thread.currentThread();
leader = thisThread;//将等待线程设为当前线程
try {
available.awaitNanos(delay);//等待delay时间
} finally {
//delay时间后等待线程还是当前线程,则清空,重新循环获取堆顶元素
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
//到这里说明结束了自旋,出队成功
//如果没有线程在等待,且堆顶还有元素,就再唤醒一个等待线程
if (leader == null && q.peek() != null)
available.signal();
//释放锁
lock.unlock();
}
}
流程:
- 加锁
- 获取堆顶元素,如果为空,则直接阻塞等待
- 如果不为空,则再判断该元素到期了没,如果到期了,就将堆顶元素出队返回
- 如果没到期,则判断是否已经有线程在等待,如果有,则阻塞等待
- 如果没有线程在等待,则将等待线程设为当前线程,并阻塞等待delay时间自动唤醒,同时清空等待线程,进入下个循环再次获取
- 如果没有线程在等待且堆顶还有元素,就再唤醒一个等待线程
- 释放锁
take方法在队列为空或未到期时会进行阻塞等待
还有一个带超时的poll(long timeout, TimeUnit unit)
,跟take逻辑基本一样,只是在堆顶为空时等待timeout时间,nanos小于延迟时间时等待nanos时间
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) {
if (nanos <= 0)
return null;
else
nanos = available.awaitNanos(nanos);
} else {
long delay = first.getDelay(NANOSECONDS);
if (delay <= 0)
return q.poll();
if (nanos <= 0)
return null;
first = null; // don't retain ref while waiting
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 && q.peek() != null)
available.signal();
lock.unlock();
}
}
六、示例
package cn.xys.syn;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
/**
* @author MaoLin Wang
* @date 2020/3/2816:46
*/
public class DelayedQueueTest {
public static void main(String[] args) {
DelayQueue<Message> queue = new DelayQueue<>();
long start = System.currentTimeMillis();
new Thread(() -> {
while (true) {
try {
System.out.println(queue.take().deadLine - start);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}).start();
queue.add(new Message(start+2000));
queue.add(new Message(start+12000));
queue.add(new Message(start+9000));
queue.add(new Message(start+6000));
queue.add(new Message(start+33000));
}
}
class Message implements Delayed {
long deadLine;
public Message(long deadLine) {
this.deadLine = deadLine;
}
@Override
public long getDelay(TimeUnit unit) {
return deadLine - System.currentTimeMillis();
}
@Override
public int compareTo(Delayed o) {
return (int) (getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS));
}
}
结果:
2000
6000
9000
12000
33000
七、总结
- 内部使用优先级队列,实现Delayed接口,将延迟时间作为优先级,早到期的早出队,因此可以用于定时任务
- 是一个阻塞队列,取一个元素后如果队列非空,还会唤醒下一个线程
- 内部使用重入锁加一个条件队列实现并发安全,条件队列用于队列为空时,部分方法如带超时的poll和take会进行阻塞等待