DelayQueue 分析
DelayQueue队列是支持延迟队列,在里面接口必须是实现Delayed
接口。下面先看看Delayed
接口
1. Delayed 接口分析
Delayed接口实现了Comparable接口,并且提供了getDelay
方法,支持返回一个数字,这个数字表示这个对象剩余的时间,在给定的单位里面,0或者负数意味着这个对象已经到期,直接返回,
也就是说,一个对象实现了这个接口,就必须要实现两个接口,一个是getDelay
一个是Comparable
public interface Delayed extends Comparable<Delayed> {
/**
* Returns the remaining delay associated with this object, in the
* given time unit.
*
* @param unit the time unit
* @return the remaining delay; zero or negative values indicate
* that the delay has already elapsed
*/
long getDelay(TimeUnit unit);
}
2. 属性分析
从这里可以发现,DelayQueue
的实现是依托于PriorityQueue。建议看看PriorityQueue分析,除了一个锁和一个Conditio之外,leader
是最引人注目的。
leader
表示线程等待队列的头元素,他是Leader-Follower
的变种实现,当一个thread变为leader的时候,提供了一个最小化的非必要的时间等待。只有在有元素delay到时间之后,这个线程才会才会变为leader,其他的线程都是在等待。leader的线程必须唤醒一个follower,在take或者poll之前,
private final transient ReentrantLock lock = new ReentrantLock();
private final PriorityQueue<E> q = new PriorityQueue<E>();
// 这里用到了Leader-Follower的模式。关于这个模式在后面的章节里面讲解
private Thread leader = null;
/**
* Condition signalled when a newer element becomes available
* at the head of the queue or a new thread may need to
* become leader.
*/
private final Condition available = lock.newCondition();
3. 主要方法分析
只要看实现了BlockingQueue
,那就说明他是一个线程安全的。
1. put 方法
put方法里面调用的还是offer
方法,并且put方法里面没有等待机制。
因为DelayQueue是依托于PriorityQueue
来实现的。关于PriorityQueue入队的操作在这里就不说了。
问题?
offer里面的唤醒操作是出于什么样的目的
可以看到,如果当前放进去的元素是最小的(堆头元素),那就说明当前的这个元素要消费了,
有下面两个方面
- 如果一开始堆中没有元素,take线程会一直等待,put一个进来说明,要唤醒take线程来操作了
- 放进去的元素是堆中最小的元素,把leader变为null,唤醒follower,表示之前的leader等待的元素不是最新的了,就需要放弃掉,leader变为null,唤醒follower,follower就会来获取锁,发现没有leader,自己变为leader,之前等待的leader就没有用了。因为`不是最新的数据了
// put方法调用了offer方法
public void put(E e) {
offer(e);
}
// 他的底层还是用的是PriorityQueue的方法,关于PriorityQueue的博客在之前已经说清楚了。
public boolean offer(E e) {
// 先上锁
final ReentrantLock lock = this.lock;
lock.lock();
try {
//入队
q.offer(e);
// 如果头节点就是当前要放入的节点,那么这个说明,当前放进去的节点就是整个堆中最小的节点。因为PriorityQueue里面是按照compareTo 方法来做判断的。Comparator来判断
if (q.peek() == e) {
// leader变为null
leader = null;
// 现在的leader要唤醒一个Follower作为leader
available.signal();
}
return true;
} finally {
lock.unlock();
}
}
2. take方法
元素的出堆这里就不说了。重点看看这里的Leader和Follower的工作和选举的过程。
一开始,先拿到堆头元素,如果没有元素,都等待。这个时候如果put线程放了一个元素,根据上面的逻辑,堆头的元素就是put线程放的元素,put线程,唤醒available,。take线程就继续让下走。
拿到堆头的元素,堆头的元素是由compareable接口决定的。从这里就可以看出来,getDelay
和compareTo
方法是不一样的功能。
如果已经到期了,直接出队,否则就需要等待。
如果没有leader,那么当前线程就是leader。根据delay
的时间来等待,注意,只有leader,才能超时时间等待,别的都是一直等待。在等待完成(处理完任务)之后,就leader变为null。再次循环,再次判断。这就有问题出现了
问题
在发现没有leader,当前线程变为leader之后,在等待的时间段,别的线程拿到锁进来,会不会处理。
不会,这个时候leader不为null,拿到锁的线程进来之后,看到leader不是null,就直接等待了。
假如说,等待时间到了之后,当前线程(A)再次获取锁的时候,有一个线程(B)也正要获取锁。这个时候还正好被B线程获取锁,拿到堆头元素,发现没有leader,这个之后堆头元素还是之前A线程等待的堆头元素,(中间没有线程操作,唯一的操作是在A。B线程获取锁的时候)。B发现堆头元素已经到期了,直接出堆,这个时候A线程就做了 一场无用功
在堆头元素需要出堆之后,还有一个finally来操作,如果没有leader并且堆头还有值,就唤醒Follower。让Follower变为leader。继续操作上面的流程。
public E take() throws InterruptedException {
// 先上锁
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
for (;;) {
// 拿到队头元素,队头元素是整个堆中最小的,注意,这里是peek,不需要出队
E first = q.peek();
// 如果fist是null,说明就没有元素,那当前线程就只能等待了
if (first == null)
available.await();
else {
// 拿到delay时间,要清楚,Comparable和Delayed是不一样的作用,
// Comparable是用于在 PriorityQueue 排序,
// Delayed 是用来做等待
long delay = first.getDelay(NANOSECONDS);
// 如果是delay到期,就直接出栈。
if (delay <= 0)
return q.poll();
// 等待的时候不需要保留引用关系,
// 断开了first对堆头的引用关系,
first = null;
//这个时候说明还是有leader的,有leader,Follower就等待。
if (leader != null)
available.await();
else {
// 如果没有leader,当前线程就是leader。
Thread thisThread = Thread.currentThread();
leader = thisThread;
try {
// 然后当前线程(leader)等待delay时间。
// 这里其实就是真正干活的节点,在Leader-Follower模型里面。
available.awaitNanos(delay);
} finally {
// 在此判断,数据是否一致,如果一致就将leader变为null,在这次操作之后,好让Follower抢锁。
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
//如果leader没有了,并且队列中还有值,就唤醒一个Follower。用来做Leader
if (leader == null && q.peek() != null)
available.signal();
// 释放锁
lock.unlock();
}
}
Leader-Follower
下面所说的通用的Leader-Follower的模型。
并发编程模型中的一种。主要有两个角色
-
Leader
leader线程等待事件发生,处理事件并且从Following中选举一个新的Leader。在处理完任务后,重新添加到Follower
-
Follower
等待变为Leader
下面是一个线程(不管是Leader还是Follower)的不同状态图。
在通用的模型里面,leader和Following是一组可以复用的线程,只对事件感兴趣,在DelayQueue
里面,take操作就是事件源。
下面的UML图表示了大体的一个基本结构
可以看到,leader和Follower都是在线程池总维护的可复用的线程,并且他们本身没有区别。
一开始发现,Thread1先来,变为了leader,thread2后来变为了follower。
当事件发生(HandleSet中有操作了)Leader线程在Follower中选举一个新的Leader(promote_new_leader),thread2变为了新的leader。thread1去处理事件(handle_event)处理完之后,最后变为了follower。
大体就是这个样子了,详细的可以看上面说的论文里面的内容,里面阐述了现在OLTP
中的问题,和解决方案。
在DelayQueue
中,这种模式的变种实现。
根据 getDelay
方法返回值,等待的操作是处理事件的过程。
如果将put和take想象为服务器接受请求和处理请求的两个操作。put就是事件。take就是handle。
不需要堵塞的时候是不需要这种模式的。