目录
1.DelayQueue
1.1整体结构
- 延迟执行,并且可以设置延迟多久之后执行
- 队列中元素将在过期时被执行,越靠近队头,越早过期
- 底层使用了优先级队列来实现,复用组合了PriorityQueue(策略者模式),让先过期的元素先执行
//队列的元素必须为Delayed本身或者子类
public class DelayQueue<E extends Delayed> extends AbstractQueue<E>
implements BlockingQueue<E> {
...
private final PriorityQueue<E> q = new PriorityQueue<E>();
// 只有一把锁,读和写同一时刻只能有一个在运行
private transient final ReentrantLock lock = new ReentrantLock();
...
}
//继承了Comparable接口,用来比较大小
public interface Delayed extends Comparable<Delayed> {
long getDelay(TimeUnit unit);
}
- DelayQueue 队列中的元素必须是实现 Delayed 接口和 Comparable 接口的,并覆写了 getDelay 方法和 compareTo 的方法才行,不然在编译时,编译器就会提醒我们元素必须强制实现 Delayed 接口。
ps:只有一把锁,读和写同一时刻只能有一个在运行
1.2放数据
public boolean offer(E e) {
final ReentrantLock lock = this.lock; //会锁住当前整个实例
// 上锁
lock.lock();
try {
// 使用 PriorityQueue 的扩容,排序
q.offer(e);
// 如果恰好刚放进去的元素正好在队列头,需要唤醒 take 的阻塞线程
// 空队列一定也会进入判断,因为一定是队列头!!!
if (q.peek() == e) {
leader = null;
available.signal();
}
return true;
} finally {
// 释放锁
lock.unlock();
}
}
DelayQueue的offer方法核心块使用了PriorityQueue的offer方法
加锁,使用PriorityQueue的offer方法判断扩容并且按序插入,如果元素恰好放在队列头就唤醒一个take线程
// PriorityQueue的offer方法:新增元素
public boolean offer(E e) {
// 如果是空元素的话,抛异常
if (e == null)
throw new NullPointerException();
modCount++;
int i = size;
// 队列实际大小大于容量时,进行扩容
// 扩容策略是:如果老容量小于 64,2 倍扩容,如果大于 64,50 % 扩容
if (i >= queue.length)
grow(i + 1);
size = i + 1;
// 如果队列为空,当前元素正好处于队头
if (i == 0)
queue[0] = e;
else
// 如果队列不为空,插入堆,从下向上冒泡
siftUp(i, e);
return true;
}
// 维护一个最小堆!!!
// 新增操作:从下向上冒泡的过程不需要左右节点在比较的!!!
// 删除操作:从上向下冒泡的过程是需要左右节点比较然后在进行交换的!!!
private void siftUp(int k, E x) {
Comparable<? super E> key = (Comparable<? super E>) x;
// k 是当前队列实际大小的位置
while (k > 0) {
// 对 k 进行减倍
int parent = (k - 1) >>> 1;
Object e = queue[parent];
// 如果 x 比 e 大,退出,把 x 放在 k 位置上
if (key.compareTo((E) e) >= 0)
break;
// x 比 e 小,继续循环,直到找到 x 比队列中元素大的位置
queue[k] = e;
k = parent;
}
queue[k] = key;
}
PriorityQueue的offer方法:先判断是否需要扩容(如果元素个数少于64每次扩容2倍,否则每次扩容1.5倍),如果队列为空那么直接在头节点新增即可,如果不为空,就需要进行堆插入从下向上进行冒泡,不断的跟父节点进行比较即可,最后返回true新增成功!
ps:因为本身就是一个堆,所以不需要在判断另一个节点的值,这一点跟从上向下冒泡不同,后者需要比较出左右节点的最值!
ps:堆排序首先需要维护一个堆(从下向上对每个非叶子节点自上向下冒泡维护一个最大堆),然后进行实际的排序(每次与堆尾部交换头节点,然后从堆头自上向下维护堆结构)
1.3拿数据
//take方法:无限阻塞
public E take() throws InterruptedException {
final ReentrantLock lock = this.lock; //获取锁,会锁住整个实例
lock.lockInterruptibly();// 可中断锁
for (;;) {
// 从队头中拿数据出来
E first = q.peek();
// 如果为空,说明队列中,没有数据,阻塞住
if (first == null)
available.await();
else {
// 获取队头数据的过期时间
long delay = first.getDelay(NANOSECONDS);
// 如果过期了,直接返回队头数据
if (delay <= 0)
return q.poll();
// 引用置为 null ,便于 gc,这样可以让线程等待时,回收 first 变量
first = null;
// leader 不为空的话,表示当前队列元素之前已经被设置过阻塞时间了
// 已经被设置阻塞时间不会进入这里吧!!!!finally不是保证重置了吗???
if (leader != null)
available.await();
else {
// 之前没有设置过阻塞时间,按照一定的时间进行阻塞
Thread thisThread = Thread.currentThread();
leader = thisThread;
try {
// 进行阻塞,等待delay延迟时间
available.awaitNanos(delay);
} finally {//一定会重置
if (leader == thisThread)
leader = null;
}
}
}finally {
if (leader == null && q.peek() != null)
available.signal();
lock.unlock();
}
}
加锁并且设置为可中断,如果队列为空就会阻塞,如果不为空会先获取延迟时间,如果延时过期(可以执行了)就删除头节点并且执行,如果没有设置阻塞时间会进行延时等待