1 DelayQueue
使用场景:让消息具有延迟消费的特性
也是基于二叉堆结构实现的,不同之处是DelayQueue是将剩余延迟时间最短的放在堆顶,继承Delayed接口,因此在使用DelayQueue时,需要实现Delayed方法,并重写两个方法。
DelayQueue底层使用了PriorityQueue,所以DelayQueue也是无界队列
public class DelayQueue<E extends Delayed> extends AbstractQueue<E>
implements BlockingQueue<E> {
}
public interface Delayed extends Comparable<Delayed> {
long getDelay(TimeUnit unit);
}
public class TestBlockingQueue implements Delayed {
@Override
public long getDelay(TimeUnit unit) {
return 0;
}
@Override
public int compareTo(Delayed o) {
return 0;
}
}
在应用上,比如外卖15分钟商家需要接单,否则,订单自动取消,可以每下一个订单就放在延迟队列中,如果规定时间内,商家没有接单,则通过消费者获取元素,然后取消订单。
核心属性分析
// 因为DelayQueue依然是阻塞队列,需要保证线程安全
private final transient ReentrantLock lock = new ReentrantLock();
// 底层使用PriorityQueue实现,底层依然是二叉堆结构
private final PriorityQueue<E> q = new PriorityQueue<E>();
// 存储等待栈顶数据的消费者
private Thread leader;
// 生产者插入数据时,不会阻塞
private final Condition available = lock.newCondition();
2 DelayQueue的写入源码分析
public boolean add(E e) {
return offer(e);
}
public boolean offer(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
//调用PriorityQueue的offer方法,会根据传入的排序方式,进行节点的上移下移操作
q.offer(e);
// 调用PriorityQueue的peek方法,如果栈顶的元素等于当前元素,则设置leader为空,并唤醒消费者线程
if (q.peek() == e) {
leader = null;
available.signal();
}
return true;
} finally {
lock.unlock();
}
}
3 DelayQueue的读取源码分析
消费者存在阻塞的情况:
-
消费者要拿到栈顶的数据,但是延迟时间还没到,此时需要等待一会儿
-
消费者要来拿数据,但是发现已经有消费者在等待栈顶数据了,这个后来的消费者也需要等待一会儿
3.1 remove方法实现原理分析
public E remove() {
E x = poll();
if (x != null)
return x;
else
throw new NoSuchElementException();
}
3.2 poll方法实现原理分析
// poll是浅尝一下,能拿到就返回,拿不到就拉倒
public E poll() {
// 消费者与生产者是一把锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 拿到栈顶数据
E first = q.peek();
// 如果元素为null ,直接返回null;如果数据的延迟时间>0,说明还没到延迟时间,元素无法返回,返回null
return (first == null || first.getDelay(NANOSECONDS) > 0)
? null
// 到达了延迟时间,此时调用优先级队列的poll方法返回数据
: q.poll();
} finally {
lock.unlock();
}
}
3.3 poll(long timeout, TimeUnit unit)实现原理分析
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
// 转为ns更精确
long nanos = unit.toNanos(timeout);
// 拿锁
final ReentrantLock lock = this.lock;
// 加锁
lock.lockInterruptibly();
try {
for (;;) {
// 获取栈顶数据
E first = q.peek();
if (first == null) {
if (nanos <= 0L)
// 栈顶为null,并且没有时间了,直接返回null
return null;
else
// 栈顶为null,但还有时间,此时调用Condition的挂起nanos时间的线程操作,挂起线程后,在生产者添加数据后唤醒
nanos = available.awaitNanos(nanos);
} else {
// 获取栈顶元素的延迟时间
long delay = first.getDelay(NANOSECONDS);
// 看当前数据的延迟时间是否到了,到了则直接执行优先级队列的poll方法,返回元素
if (delay <= 0L)
return q.poll();
//延迟时间未到,消费者需要等待
// 消费者可以等待的时间,如果到了,直接返回null
if (nanos <= 0L)
return null;
// first赋值为null
first = null; // don't retain ref while waiting
// 等待的时间小于剩余的延迟时间,消费者直接挂起,暂时无法拿到元素
// 如果leader不为null,说明有消费者在等待栈顶数据,直接挂起即可
if (nanos < delay || leader != null)
nanos = available.awaitNanos(nanos);
else {
// 当前消费者的阻塞时间可以拿到数据,并且没有其他消费者在等待栈顶数据
// 拿到当前消费者的线程对象
Thread thisThread = Thread.currentThread();
// 设置leader为当前线程
leader = thisThread;
try {
// 让当前消费者阻塞这个元素的延迟时间
long timeLeft = available.awaitNanos(delay);
// 重新计算当前消费者剩余的可阻塞时间
nanos -= delay - timeLeft;
} finally {
//当前线程设置leader为null
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
// 最后如果leader为null 并且有限队列取出的栈顶数据不为空,则唤醒线程
if (leader == null && q.peek() != null)
//需要再次唤醒后继消费者,避免队列有元素,但没有消费者唤醒的问题
available.signal();
lock.unlock();
}
}
3.4 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 <= 0L)
// 延迟时间小于等于0,直接调用优先级队列的poll方法,返回元素
return q.poll();
// 设置元素为null
first = null; // don't retain ref while waiting
if (leader != null)
// leader不等于空,说明有消费者在等待元素,此时调用Condition的等待线程挂起的方法
available.await();
else {
// 获取当前线程
Thread thisThread = Thread.currentThread();
// 设置当前线程为消费者,赋值给leader
leader = thisThread;
try {
// 调用Condition的等待线程挂起延迟时间的方法
available.awaitNanos(delay);
} finally {
// 当前消费者线程重置leader为null
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
// 最后如果leader为null 并且有限队列取出的栈顶数据不为空,则唤醒线程
if (leader == null && q.peek() != null)
//需要再次唤醒后继消费者,避免队列有元素,但没有消费者唤醒的问题
available.signal();
lock.unlock();
}
}