DelayQueue是一个支持延时获取元素的无界阻塞队列
public class DelayQueue<E extends Delayed> extends AbstractQueue<E>
implements BlockingQueue<E> {
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();
}
可以看到其内部维护了一个PriorityQueue
对象,根据后续的方法源码解读中可以发现,此队列是DelayQueue
最终保存元素的队列。因为PriorityQueue
是一个无界的队列,所以DelayQueue自然也实现了无界队列。
类似其他的队列实现,其提供了add、put和offer三个方法进行元素的添加。
add和put
public boolean add(E e) {
return offer(e);
}
public void put(E e) {
offer(e);
}
但是实际上根据源码可以发现,add和put方式内部调用的是offer方法。
offer
此方法是DelayQueue最终添加元素的最终逻辑。其内部主要使用的是PriorityQueue
的方法
public boolean offer(E e) {
final ReentrantLock lock = this.lock;
lock.lock();
try {
q.offer(e);
// 当插入的元素为队首,证明之前没有数据
// 此时可能存在阻塞的读取线程
if (q.peek() == e) {
// 当读取线程为空
leader = null;
// 唤醒相关线程
available.signal();
}
return true;
} finally {
lock.unlock();
}
}
可以看到其入队的方法主要依托于PriorityQueue
并没有其他特殊的内容。
上面介绍过DelayQueue是一个支持延迟获取元素的队列,而其提供的三个查询元素的方法中。peek
只是实现了简单的数据查询,其延迟获取的方法主要在poll
和take
中
peek
public E peek() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
return q.peek();
} finally {
lock.unlock();
}
}
poll
public E poll() {
final ReentrantLock lock = this.lock;
lock.lock();
try {
E first = q.peek();
// first.getDelay(NANOSECONDS) 获取队首元素的超时时间
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;
// 使用lockInterruptibly方式获取锁
lock.lockInterruptibly();
try {
// 进入循环
for (;;) {
E first = q.peek();
// 如果为空则等待
if (first == null)
available.await();
else {
// first.getDelay(NANOSECONDS)
long delay = first.getDelay(NANOSECONDS);
// 超时则直接出队
if (delay <= 0)
return q.poll();
// 释放引用
first = null; // don't retain ref while waiting
// 此时当leader为空的时候会被当前线程赋值,
// 所以当leader不为空的时候,则代表有其他线程在操作内容
// 进行线程阻塞,这个时候因为leader只能有一个值,所以保证了只有一个线程线程去等待到时唤醒,避免大量唤醒操
if (leader != null)
available.await();
else {
Thread thisThread = Thread.currentThread();
// 赋值为当前线程
leader = thisThread;
try {
// 进行超时的阻塞
available.awaitNanos(delay);
} finally {
// 等待结束后释放leader
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
// leader为null并且队列不为空,说明没有其他线程在处理,且存在值
// 则唤醒available的锁
if (leader == null && q.peek() != null)
available.signal();
// 释放全局锁
lock.unlock();
}
}
其出队流程可以描述为:
这里需要关注的几个地方
first = null; // don't retain ref while waiting
E first = q.peek();
导致线程1持有队首元素引用。E first = q.peek();
代码已经执行,导致其被线程2持有,假如线程阻塞完毕了,获取列首元素成功,出列。这个时候列首元素应该会被回收掉,但是问题是它还被线程2持有着,所以不会回收,就会造成内存泄漏。在出队的方法中我们看到方法使用Delayed进行超时判断。此接口是延迟队列进行延迟的主要逻辑。所以DelayQueue要求队列中的元素必须实现Delayed接口
public interface Delayed extends Comparable<Delayed> {
long getDelay(TimeUnit unit);
}
该方法返回与此对象相关的的剩余时间。同时可以看到此接口继承了Comperable接口。所以实现Delayed接口的类存在一个排序逻辑,然后配合PriorityQueue
队列可以保证在取出的元素的延时时间是有序的。
因为DelayQueue是基于Delayed
的接口进行超时判断,所以元素需要继承此接口,下面一个简单使用此队列的例子
import java.util.concurrent.DelayQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;
/**
* @author daify
*/
public class Test {
public static void main(String[] args) throws InterruptedException {
DelayQueue delayQueue = new DelayQueue();
long l = System.currentTimeMillis();
Item item = new Item(l+200,"item");
Item item2 = new Item(l+100,"item2");
Item item3 = new Item(l+150,"item3");
Item item4 = new Item(l+300,"item4");
delayQueue.add(item);
delayQueue.add(item2);
delayQueue.add(item3);
delayQueue.add(item4);
int size = delayQueue.size();
long time = 0;
for (int i = 0; i < size; i++) {
long l1 = System.currentTimeMillis();
System.out.println(l1 - time);
time = l1;
System.out.println(((Item)delayQueue.take()).getName());
}
}
}
class Item implements Delayed {
private String name;
private Long cancelTime;
public Item(Long cancelTime,String name) {
this.cancelTime = cancelTime;
this.name = name;
}
@Override
public long getDelay(TimeUnit unit) {
long l = unit.convert(cancelTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
return l;
}
@Override
public int compareTo(Delayed o) {
return this.getCancelTime().compareTo(((Item) o).getCancelTime());
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Long getCancelTime() {
return cancelTime;
}
public void setCancelTime(Long cancelTime) {
this.cancelTime = cancelTime;
}
}
输出内容
item2
1000
item3
499
item
500
item4
此时可以看到在元素插入队列的时候,元素就已经根据时间进行重新排序了。所以取出的顺序是根据compareTo
的结果计算的。
延迟队列主要应用的场景:
个人水平有限,上面的内容可能存在没有描述清楚或者错误的地方,假如开发同学发现了,请及时告知,我会第一时间修改相关内容。假如我的这篇内容对你有任何帮助的话,麻烦给我点一个赞。你的点赞就是我前进的动力。