DelayQueue
: JDK提供的延迟获取元素的无界阻塞队列。
应用场景:
- 缓存有效期: 能取到的时候过期.
- 定时任务: 能取到的时候执行。
DelayQueue
的两个特性:
无界: 说明底层至少使用了链表。
阻塞: 为底层实现时,利用访问线程干活埋下伏笔(leader
)。
底层实现机制
底层用一个优先级队列存储delay
元素(比如定时任务),排序依据是延迟的时长。
可以看出,这里用的是一个支持无界阻塞的优先级队列实现(例如PriorityBlockingQueue
)。
时间复杂度: O(logN)
。由于使用的是优先级队列(最小堆),插入新元素(任务)的时间复杂度是O(logN)
。
take
流程:
lock.lockInterruptibly()
;- delay = 队首元素延时;
- delay<=0: 取出该元素;
- delay>0: 尝试设置自己线程为
leader
,然后等待delay时间。 finnally: lock.unlock()
;
这里leader
的职责是等待delay
时间,以及唤醒其他等待线程。
注意第四步里, 由于leader
只能有一个,所以如果尝试成为leader
失败,就直接await
就好,会被leader
叫醒的。
offer
: offer就是简单地把元素插入堆中。如果正好是延迟最小,则此时之前的leader
等待时间太长了,因此需要叫醒所有等待线程。
总结
DelayQueue
优点:
资源开销小: 本质上只需一个线程负责等待;每次等待间隔都是delay时间,cpu空转少。
缺点
插入删除延时元素时间复杂度为O(logN),对于成千上万的延时任务时间开销大,有待优化。
优化思路:
如果要比O(logN)
更好,很自然的思路就是使用哈希表O(1)
,空间换时间。
优化方案
需求: 降低插入删除延时元素的时间复杂度。
方案:
- 保留优先级队列,但里面的元素改成链表引用(比如从
Task
到List<Task>
); - 引入辅助哈希表,保存的也是链表引用(
List<Task>
),hash函数的输入为延时时长。
这里即使List<Task>
长期只有1个元素也没有关系,这里只是为了起到C++中指针的效果,方便从哈希表里直接修改(插入删除)优先级队里的数据。
take
/offer
流程: 与之前相似,唯一不同就是取到List<Task>
后,遍历其中所有Task
。
插入延时元素(任务):(删除也类似)
- 根据延时,通过哈希函数找到哈希表中的槽位;
- 如果以前有过相同延时: 直接插入List;
- 如果以前没有相同延时: 新建List,并且插入优先级队列。
可以看出这里优化的重要条件是能合并多个Task到List。
为了达到这个条件,可以使用进一步的优化方案: 时间轮。