无头队列怎么实现:DelayQueue用法、实现及Leader-Follower模式
作者:吴潇 职位:Java软件工程师
原创声明
这是本人署名原创文章,未经许可不支持转载且请勿抄袭。本公众号的所有文章均为本人原创。为了容易理解和记忆,文章以图解为主、代码为辅,忽略不重要细节,但保留关键细节。如果您感兴趣,欢迎关注!
如果需要给对象关联一个延迟时间,让这个对象必须在等待一段时间之后才可以被使用(处理),否则这个对象对使用者来说不可见(例如,支付宝的快速到账和延迟到账功能),这样的任务怎么实现?最简单的方法是使用JDK并发工具中的Delay接口和DelayQueue队列。
用Delayed接口标记延迟对象
首先,需要把类标记成支持延迟特性的,即实现Delayed接口。Delayed接口要求实现getDelay和compareTo方法。其中,getDelay方法负责控制对象如何延迟,它应该返回一个剩余时间;compareTo负责定义不同延迟对象之间的相对顺序(用于堆排序)。例如:
getDelay方法的返回值将用于排序,因此,它必须与compareTo保持一致。其实,我们可以在getDelay方法中实现一些特定逻辑,以灵活控制延迟时间,例如延迟一个固定的时间,延迟一个随机的时间,或者根据流量/负载等动态调整延迟时间。实现getDelay的时候要注意根据传入的TimeUnit返回对应不同单位的时间数值。
将Delayed对象放入DelayQueue(延迟队列)
DelayQueue是JDK并发包提供的延迟任务队列,是一个无界阻塞队列,只能存储Delayed对象。当Delayed对象进入DelayQueue之后,只有其延迟时间结束才可以从队列取出,否则对于外部来说不可见。队列头部存放尚未被取出的、最早到达结束时间的Delayed对象,如果队列中的对象全都没到达结束时间(getDelay返回值大于0),则队列头部是空的(poll将返回null)。此时,队列中有对象但是看上去是没有头部的,因而称为无头队列。
但是注意,DelayQueue中的对象不是按照getDelay返回值从小到大排序的,它实际上是一个堆(堆顶元素最小)。对这个队列的任何遍历操作都不保证是排好序的。
如何使用DelayQueue
生产者负责创建Delayed对象,并且把对象放入DelayQueue(可以让每个对象的getDelay方法执行不同的代码,也可以让它们都执行相同的代码、只是参数不同)。生产者则从DelayQueue取出对象直接使用,不必判断延迟时间是否结束。假如对象放入DelayQueue之后,对象的延迟时间是动态变化的,则需要把这个对象从队列删除然后再重新放进队列(注意,JDK官方文档没有介绍这个问题)
DelayQueue的实现原理
DelayQueue内部主要依赖PriorityQueue(非线程安全的)实现,即它把Delayed对象放入PriorityQueue,由PriorityQueue保证延迟时间最小的对象总是位于头部。当往DelayQueue中放入对象时,把对象放入内部的PriorityQueue即可;当从DelayQueue取出对象时,先到查看一下内部PriorityQueue的队列头部的对象(通过peek),如果getDelay小于等于0才会返回,否则均返回null。在放入对象(add/offer/put)和取出对象的时候(poll/take),调整线程的等待状态,这是基于Leader-Follower模式实现的,见后文分析。
DelayQueue是一个线程安全的类,所有操作都是通过ReentrantLock保证线程安全性,因而不适合高并发的场景。线程的等待和阻塞是用与ReentrantLock关联的条件队列实现的。
Leader-Follower模式
因为可能有多个线程在DelayQueue上进行等待(从头部获取元素),所以,为了减少每个等待线程多次调用getDelay的计算量,DelayQueue采用了Leader-Follower模式,即只让一个Leader线程进行timed wait(根据getDelay的返回值计算等待时间),而其他Follower线程进行indefinite wait并且需要被Leader线程唤醒(通过signal唤醒)。
Leader-Follower模式的目标:
- 减少getDelay调用次数,让getDelay只被Leader线程调用一次。
- 如果在等待的过程中有新对象进入队列,则重新计算等待时间。Leader-Follower模式保证只需要调整Leader线程的等待时间,不需要调整其他Follower线程。
Leader-Follower模式在DelayQueue中的实现:
- 当队列为空的时候,所有线程都进行indefinite wait,此时没有Leader。
- 当放入一个对象且它位于成为PriorityQueue头部时,重新选择Leader并唤醒。
- 当Leader线程成功从队列取走对象之后,如果PriorityQueue不为空,则唤醒一个Follower线程让它成为新Leader。
- 当一个线程从DelayQueue取对象时,如果此时有对象则直接取出并且返回;如果没有,则判断是否有Leader,没有Leader则自己成为Leader并进入timed wait,如果已经有Leader,则进入indefinite wait。
通过使用Leader-Follower模式,DelayQueue减少了对象的getDelay方法的调用开销,并且还保证队列中的对象动态发生变化之后,等待线程依然能及时被唤醒并从队列取出对象,实时性得到了保证。
最后再强调一下,如果Delayed接口的getDelay方法的返回值是动态变化的,那么一定要在getDelay发生变化之后,把这个对象从DelayQueue删除,然后再重新放进去。DelayQueue不能自动处理getDelay会发生变化的情况。