DelayQueue是一个无界的阻塞队列,是一个延时队列,它存储的元素必须实现Delayed接口,其中的元素只有在到期时才能被消费者取走,它底层是使用的PriorityQueue,PriorityQueue底层采用数组实现的二叉堆,队列中的元素根据到期时间进行排序,剩余时间最少的放在堆顶。
因为它几乎趋近于无界,因为底层数组的最大容量限制是Int最大值减8,所以生产者往队列添加元素的时候不会出现阻塞的情况,队列满了就进行扩容
消费者会阻塞
原因1 消费者要取堆顶的元素,堆顶元素还没到期,那么就需要等元素到期才能取走
原因2 消费者要来拿数据,但是发现已经有消费者在等待堆顶数据了,这个后来的消费者也需要等待一会
通过下面的例子来理解一下队列的使用
package main.java.List;
import java.util.*;
import java.util.concurrent.*;
public class TestList {
public static void main(String[] args) throws ExecutionException, InterruptedException {
DelayQueue<Task> delayQueue = new DelayQueue();
delayQueue.add(new Task("A",1000L));
delayQueue.add(new Task("B",3000L));
delayQueue.add(new Task("C",7000L));
delayQueue.add(new Task("D",5000L));
System.out.println(((Task)delayQueue.take()).getName() + ","+ new Date());
System.out.println(((Task)delayQueue.take()).getName() + ","+ new Date());
System.out.println(((Task)delayQueue.take()).getName() + ","+ new Date());
System.out.println(((Task)delayQueue.take()).getName() + ","+ new Date());
}
/**
* 定义加入队列的元素
*/
static class Task implements Delayed {
// 元素名称 方便区分
private String name;
// 什么时间点执行
private Long delayTime;
// time 是告诉队列这个元素多少时间后可以被取走,比如 1000(1秒),表示元素1秒中后可以被取走,
// 那么最终delayTime 就是当前时间+time
public Task(String name, Long time) {
this.name = name;
this.delayTime = System.currentTimeMillis()+time;
}
// getDelay 用于取元素时作判断,用元素可以被取走的时间-当前时间,如果<=0,说明时间已经到了,可以取走
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(delayTime-System.currentTimeMillis(), TimeUnit.MILLISECONDS);
}
// compareTo存放元素时进行比较,对元素进行排序
@Override
public int compareTo(Delayed o) {
return (int) (this.delayTime-((Task)o).getDelayTime());
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Long getDelayTime() {
return delayTime;
}
public void setDelayTime(Long delayTime) {
this.delayTime = delayTime;
}
}
}
执行结果
1、类信息
通过Delayed Comparable 俩个接口,让元素有了比较和延迟的功能
public class DelayQueue<E extends Delayed> extends AbstractQueue<E>
implements BlockingQueue<E>{
// DelayQueue中的元素,需要继承Delayed接口
}
public interface Delayed extends Comparable<Delayed> {
// 获取元素时,用于判断元素是否可取
long getDelay(TimeUnit unit);
}
public interface Comparable<T> {
// 比较的接口
public int compareTo(T o);
}
2、重要属性
// DelayQueue属于阻塞队列,需要保证线程安全。只有一把锁,生产者和消费者使用的是一个lock
private final transient ReentrantLock lock = new ReentrantLock();
// 底层使用的是PriorityQueue
private final PriorityQueue<E> q = new PriorityQueue<E>();
//leader一般会存储等待栈顶数据的消费者,在写入和消费的过程中,会设置leader
private Thread leader = null;
// 等待队列,生产者是不会阻塞的,所以这个Condition是给消费者使用的,
// 比如堆顶元素还没到期,此时消费者就需要阻塞
private final Condition available = lock.newCondition();
3、构造方法
/**
* 无参构造 创建一个空队列
*/
public DelayQueue() {}
/**
* 有参构造 可以传入一个集合,集合中的元素会被添加到队列中
*/
public DelayQueue(Collection<? extends E> c) {
this.addAll(c);
}
4、写入方法
Delay是无界的,数组可以动态扩容,不需要关注生产者的阻塞问题,他就没有阻塞问题
通过下面的代码,所有写入操作,都是走的offer,研究清楚offer就明白了
public boolean add(E e) {
return offer(e);
}
public void put(E e) {
offer(e);
}
public boolean offer(E e, long timeout, TimeUnit unit) {
return offer(e);
}
public boolean offer(E e) {
// 加锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 调用PriorityQueue的offer方法
// 这里会根据之前重写Delayed接口中的compareTo方法做排序,然后调整上移和下移操作
q.offer(e);
// 取堆顶元素,如果堆顶元素是刚刚添加进来的 将leader置为空,为什么呢,因为这个元素刚刚添加进来,
// 肯定没有消费者在等着当前这个元素,leader如果不为空,那它也只是在等待之前的堆顶,但是现在的
// 堆顶已经变了,所以置为空,并且唤醒等待的消费者,让他们重新来获取锁,等待当前的堆顶元素
if (q.peek() == e) {
leader = null;
// 唤醒消费者,避免刚刚插入的数据的延迟时间出现问题。
available.signal();
}
// 返回
return true;
} finally {
// 释放
lock.unlock();
}
}
5、读取方法
public E poll() {
// 加锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 查看堆顶元素,如果元素为空 或者元素未到期,直接返回null
E first = q.peek();
if (first == null || first.getDelay(NANOSECONDS) > 0)
return null;
else
// 到这说明元素不为null,并且已经达到了延迟时间,直接调用PriorityQueue的poll方法
return q.poll();
} finally {
lock.unlock();
}
}
下面的poll(long timeout, TimeUnit unit),是允许阻塞的
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了,直接返回null
if (nanos <= 0)
return null;
else
// 如果还有剩余时间则等待nanos时间
// 这里挂起线程后,说明队列没有元素,在生产者添加数据之后,会唤醒
nanos = available.awaitNanos(nanos);
} else {
// 队列有元素 获取堆顶元素的剩余时间
long delay = first.getDelay(NANOSECONDS);
// 有数据的话,先获取数据现在是否可以执行,延迟时间是否已经到了指定时间
if (delay <= 0)
// 时间已经到了,则直接获取
return q.poll();
// =========================================================
// 如果队列中的元素还没到期,判断消费者的剩余时间是否还能等
if (nanos <= 0)
// nanos 小于等于0,不能等,直接返回null
return null;
// ==================延迟时间没到,消费者可以等一会===================
// 把first赋值为null
first = null;
// 如果等待的时间,小于元素剩余的延迟时间,消费者直接挂起。
// 反正暂时拿不到,但是不能保证后续是否有生产者添加一个新的数据,我是可以拿到的。
// 如果已经有一个消费者在等待堆顶数据了,我这边不做额外操作,直接挂起即可。
if (nanos < delay || leader != null)
nanos = available.awaitNanos(nanos);
else {
// 走到这里,说明当前消费者的阻塞时间可以拿到数据,并且没有其他消费者在等待堆顶数据
// 将leader设置为当前线程
Thread thisThread = Thread.currentThread();
leader = thisThread;
try {
// 让当前消费者阻塞元素的延迟时间
long timeLeft = available.awaitNanos(delay);
// 重新计算当前消费者剩余的可阻塞时间
nanos -= delay - timeLeft;
} finally {
// 到了时间,将leader设置为null
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
// 没有消费者在等待元素,队列中的元素不为null
if (leader == null && q.peek() != null)
// 只要当前没有leader在等,并且队列有元素,就需要再次唤醒消费者。、
// 避免队列有元素,但是没有消费者处理的问题
available.signal();
// 释放锁
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);
// 判断延迟时间是不是已经到了
if (delay <= 0)
// 基于优先级队列的poll方法返回
return q.poll();
first = null;
// 如果有消费者在等,就正常await挂起
if (leader != null)
available.await();
// 如果没有消费者在等堆顶数据,我来等
else {
// 获取当前线程
Thread thisThread = Thread.currentThread();
// 设置为leader,代表等待堆顶的数据
leader = thisThread;
try {
// 等待指定(堆顶元素的延迟时间)时长,
available.awaitNanos(delay);
} finally {
if (leader == thisThread)
// leader赋值null
leader = null;
}
}
}
}
} finally {
// 避免消费者无限等
// 一般是其他消费者拿到元素走了之后,并且延迟队列还有元素,就执行if内部唤醒方法
if (leader == null && q.peek() != null)
available.signal();
// 释放锁
lock.unlock();
}
}