DelayQueue是一个无界的阻塞队列,它的元素实现了Delayed接口,并且元素只有在delay到期以后才能取出,如果元素没有到期,执行poll方法将返回null。
内部是一个最小堆,堆顶永远是最先"到期"的那个元素,如果堆顶元素没有到期,即使线程发现队列中有元素,也不能将其出队。
DelayQueue需要依赖元素对Delayed接口正确实现,即保证到期时间短的Delayed元素.compareTo(到期长的Delayed元素)<0,这样可以让到期短的Delayed元素排在队列前面。
先看了例子:
我们可以取出元素时间相对于Log.d("tanyonnglin","====================================");分别是传入的时间数字1,2,5,7,9。每次都取队列头部。
成员属性:
1 非公平锁
2 使用PriorityQueue存储元素,是个最小堆。
3 Leader-Follower线程模式中的Leader,它总是等待获取队首。
4 不管哪种线程(Leader或Follower线程)都将阻塞在这个条件队列上,但Follower是无限的阻塞
Leader-Follower:
在队列中的处于队首的Delay元素,由于还没到期,只能暂时等待它到期,这种暂时等待需要用到Condition.awaitNanos。虽然第一个来的线程是可以明确要等待队首元素多久(通过getDelay),但第二个或者后续来的线程就不确定等多久了,其实应该让他们去等待排名第二或以后的元素,但是优先队列事个最小堆,只能确定最小元素,确定不了第二小的元素。
所以就干脆让第二个或后续来的线程无限等待(Condition.await),但我们让第一个线程负责唤醒沉睡在条件队列上的线程。因为第一个线程总是使用Condition.awaitNanos,所以不会造成条件队列上的线程无期限地一直等待,第一个线程总是等待获取堆顶,当它出队成功后,再唤醒后面的线程去获得新堆顶。
上面说的第一个线程其实就是Leader-Follower模式中的Leader了,它总是会以Condition.awaitNanos的方式阻塞,这保证了它不会一直沉睡,而其他线程就是所谓的Follower,当它们检测到Leader的存在时,则可以放心使用Condition.await,反正有Leader线程会唤醒啊。
入队:
1 lock.lock同步
2 添加到优先队列PriorityQueue中
3 如果添加的就是头节点,特殊处理,
说明新元素入队后就称为了堆顶,说明最小元素更新了,这也说明之前的leader(如果存在的话)调用的awaitNanos的不准确了,那么干脆重新清空leader线程,重新唤醒线程重新选出leader。
take():
首先第214行是一个死循环,继续往下看可以发现221行return q.poll()是唯一的出口终止循环,
1: 215-217行 如果队列为空,线程阻塞。
2 :后续代码就是队列不为空情况,存在两者情况。
这里我们来举例分析下:
假设当前队列存在5个元素,delay时间(Delay接口的getDelay()方法返回的值)分别为1,7,5,9,2,单位是TimeUnit.SECONDS也就是秒,这样队列头就是队列时间为2的元素,并且队列头delay时间还没有过期,现在存在3个线程同时调用take方法,来获取队列头,我们来分析这个过程。
1 假设线程1拿到了lock锁,执行到220行时,发现delay>0,肯定是无法立即返回的。
2 执行到223行时,判断leader是否为Null,此时leader是为Null的。
3 将当前线程也就是线程1赋给leader,然后线程阻塞delay,释放锁。
4 此时线程2如果拿到了锁,发现leader不为null,执行224行代码。立即阻塞,然后释放锁。
5 线程3同线程2一样
6 当线程1阻塞时间到达后,会执行231-233行代码,在死循环中再次执行到220行代码时,此时delay<=0,退出循环,在返回队列头之前,会执行finally块中通知其他线程唤醒
7 假设线程2被唤醒,那么步骤和线程1一样的类似。
这里的leader线程代表着等待队列头时间到达的线程,leader线程永远等待队列头的超时时间,除了leader线程的其他线程均无限等待直到收到唤醒通知,才有机会成为leader线程。
结论:
DelayQueue类特性,在生产者-消费者模型中,只有在特定元素过期后才能将他们消费,另一个用途在于线程池,提交任务以后延时执行。
网友评论