JUC源码解析-阻塞队列-DelayQueue

DelayQueue是一个支持延时获取元素的无界阻塞队列。队列使用PriorityQueue来实现。队列中的元素必须实现Delayed接口,在创建元素时可以指定多久才能从队列中获取当前元素。只有在延迟期满时才能从队列中提取元素。我们可以将DelayQueue运用在以下应用场景:

  • 缓存系统的设计:可以用DelayQueue保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦能从DelayQueue中获取元素时,表示缓存有效期到了。
  • 定时任务调度。使用DelayQueue保存当天将会执行的任务和执行时间,一旦从DelayQueue中获取到任务就开始执行,从比如TimerQueue就是使用DelayQueue实现的。

队列中的Delayed必须实现compareTo来指定元素的顺序。比如让延时时间最长的放在队列的末尾。

关于使用:java并发之DelayQueue实际运用示例

关于其底层依赖的优先级队列PriorityQueue 其实就是堆算法,在上一篇 PriorityBlockingQueue 中分析过该算法,关于堆算法推荐 排序六 堆排序

Delayed

public interface Delayed extends Comparable<Delayed> {

    long getDelay(TimeUnit unit);
}

TimeUnit

public enum TimeUnit {
    /**
     * Time unit representing one thousandth of a microsecond
     */
    NANOSECONDS {
        public long toNanos(long d)   { return d; }
        public long toMicros(long d)  { return d/(C1/C0); }
        public long toMillis(long d)  { return d/(C2/C0); }
        public long toSeconds(long d) { return d/(C3/C0); }
        public long toMinutes(long d) { return d/(C4/C0); }
        public long toHours(long d)   { return d/(C5/C0); }
        public long toDays(long d)    { return d/(C6/C0); }
        public long convert(long d, TimeUnit u) { return u.toNanos(d); }
        int excessNanos(long d, long m) { return (int)(d - (m*C2)); }
    },
    MICROSECONDS
    MILLISECONDS
    SECONDS
    MINUTES
    HOURS
    DAYS
    ......

DelayQueue

public class DelayQueue<E extends Delayed> extends AbstractQueue<E>
    implements BlockingQueue<E> {
	// 锁
    private final transient ReentrantLock lock = new ReentrantLock();
    // 底层以理优先级队列 PriorityQueue
    private final PriorityQueue<E> q = new PriorityQueue<E>();

	/**
	代表等待队列头元素的线程。这种Leader-Follower模式的变体用于最小化不必要的定时等待。
	当一个线程成为领头线程时,它的等待时间即是当时堆顶元素距到期的时间,而其他线程则无限期地等待。
	在从take()或poll(…)返回之前,会发出signal信号给等待的线程,除非有其他线程在等待期间成为leader线程。
	每当队列的头替换为具有较早到期时间的元素时,leader字段将被重置为空值而无效,并且会发出signal信号给等待线程。
	因此,等待线程苏醒后可能会成为leader,具体看下面代码的分析
	*/
    private Thread leader = null;

     // 等待线程在两处被 signal : 1,新的堆顶元素出现。2,leader为null,需要一个新的leader线程时
    private final Condition available = lock.newCondition();

延迟队列,它的每个对象都有自己的到期时间,按照你定的比较规则构成一个最小堆,堆顶是优先级最大的对象。

如果你的比较规则是到期时间,那么堆顶就是最快到期的,如果不是那么堆顶就可能不是最快到期的,也就是堆里可能存在已到期的对象。

Leader-Follower 模式,来看看其实现主体 take 方法:

    public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            for (;;) { // 注意这里
                E first = q.peek(); // 探测堆顶
                if (first == null) // 数组为空,阻塞等待
                    available.await();
                else {
                    long delay = first.getDelay(NANOSECONDS); // 是否超时
                    if (delay <= 0)
                        return q.poll(); // 到期直接取出
                    first = null; // 置为null,防止线程等待期间一直只有该对象
                    if (leader != null) // 已有leader线程,等待阻塞
                        available.await();
                    else {
                    	// 将当前线程置为 leader,等待直到堆顶到期
                        Thread thisThread = Thread.currentThread();
                        leader = thisThread;
                        try {
                            available.awaitNanos(delay);
                        } finally { // 当leader恢复执行,若leader仍是该线程则置空
                            if (leader == thisThread)
                                leader = null;
                        }
                    }
                }
            }
        } finally {
        	// leader 为null,选取下一个 leader 。
            if (leader == null && q.peek() != null)
                available.signal();
            lock.unlock();
        }
    }

该方法实现了 leader 线程的等待,新 leader 线程的选取,堆顶元素的获取操作。

Leader-Follower 模式是为了尽可能缩短不必要的等待时间,如何做到的 ?

1,leader 线程只阻塞一定的时间,awaitNanos(delay),delay 为当时的堆顶元素距离到期的时间,并发情况是复杂的,堆顶可能时常在变,那么 leader 的目的又在哪?leader 确保当堆顶到期后一定会有线程来取它,那么过时的 leader 是浪费吗?不是,堆顶到期的leader 并非立刻执行,它首先是回到AQS的同步队列中排对等待,那么在这段等待时间,那些过时的 leader 线程的节点可能穿插在其中,先一步被唤醒就能提前将堆顶取出,这样就节约了时间。

2,ReentrantLock 是非公平模式,也就是新线程可以插队。比如堆顶到期了,其 leader 线程节点回到 AQS 的同步队列中等待被唤醒执行,可能需要一段时间,由于允许插队则新的取线程可能插队成功,在leader 线程之前取出堆顶,也就节约了堆顶的等待时间。

来分析下 leader 线程恢复执行会面对哪些情况?

leader 线程并非是为了取出当初与它对应的那个堆顶元素设计的,我们看上面的代码,当leader 线程苏醒,首先检查 leader 字段是否仍指向自己(offer 方法若更新了堆顶会将leader字段指控,下面再分析),若是则置空,为什么?为了给新堆顶选出 leader 线程,也就确保了新堆顶到期会有取线程来取它。

并发下情况是复杂的,苏醒的 leader 线程会遭遇那些情况呢?

1)堆顶仍是当初那个没变,取出,退出循环来到 finally 块,发出 signal 信号,下一个 leader 可能是插队的新线程或是被信号唤醒的线程。
2)leader 仍指向该线程,不过堆顶已改变,原因是 leader 在同步队列等待过程中被新的取线程插队,取走了原堆顶,这也没什么关系,上面说了 leader 设计并非是与特定堆顶绑定,当其苏醒你可以将它当作一个普通的取操作线程:查看堆顶能否取出,能就取出然后发出signal 信号,之后执行的线程会成为新堆顶的 leader,当然前提是堆顶未到期;若是不能取出就看新堆顶有无 leader,有则当前线程 await 等待,否则当前线程成为新堆顶的 leader。
3)堆顶 ,leader 都已改变,因为 offer 方法插入了新堆顶,他会置空 leader 字段并发出 signal 信号,为什么?为了尽快为新堆顶选出 leader 从而保证当其到期时有线程来取它。

offer

    public boolean offer(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            q.offer(e);
            // 若原先数组为空或新插入的 e 优先级最高成为堆顶
            // leader置为null,发出signal信号。为什么?
            //为了尽快为新堆顶选出 leader 从而保证当其到期时有线程来取它
            if (q.peek() == e) {
                leader = null;
                available.signal();
            }
            return true;
        } finally {
            lock.unlock();
        }
    }

add,put

都是调用的 offer

    public boolean add(E e) {
        return offer(e);
    }

    public void put(E e) {
        offer(e);
    }

poll

poll 操作是不阻塞的,它首先探测堆顶元素,若其到期则弹出堆顶,否则返回null

    public E poll() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            E first = q.peek();
            if (first == null || first.getDelay(NANOSECONDS) > 0)
                return null;
            else
                return q.poll();
        } finally {
            lock.unlock();
        }
    }

remove

不管是否到期都删除它。

    public boolean remove(Object o) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            return q.remove(o);
        } finally {
            lock.unlock();
        }
    }

你可能感兴趣的:(JUC,多线程,JUC源码解析)