Java并发编程(十四)DelayQueue

63 阅读4分钟

1 DelayQueue的应用

DelayQueue是无界队列,队列中的元素要实现Delayed接口,重写getDelay和compareTo方法

  • getDelay:什么时候可以出队列

  • compareTo:存放到队列时,放在二叉堆的哪个位置

用一个类来模拟延迟队列中的元素

class Task implements Delayed{

    private String name;

    /**  执行时间 (单位毫秒) */
    private Long time;

    /**
     *
     * @param name  任务名称
     * @param delayTime  传入延迟时间
     */
    public Task(String name, Long delayTime) {
        this.name = name;
        this.time = System.currentTimeMillis() + delayTime;
    }

    /** 任务可以出队列的核心方法 */
    @Override
    public long getDelay(TimeUnit unit) {
        return unit.convert(time - System.currentTimeMillis(),TimeUnit.MILLISECONDS);
    }

    /** 通过这个方法,来比较,将任务存放到二叉堆的指定位置 */
    @Override
    public int compareTo(Delayed o) {
        // 基于执行时间比较
        return (int) (this.time - ((Task)o).getTime());
    }
}
  • 延迟时间到了,才可以将元素取出来。
public static void main(String[] args) throws InterruptedException {
    DelayQueue queue = new DelayQueue();
    queue.offer(new Task("A",4000L));
    queue.offer(new Task("B",2000L));
    queue.offer(new Task("C",3000L));
    queue.offer(new Task("D",1000L));

    System.out.println(queue.take());
    System.out.println(queue.take());
    System.out.println(queue.take());
    System.out.println(queue.take());
}

// 输出结果
Task(name=D, time=1684310000097)
Task(name=B, time=1684310001097)
Task(name=C, time=1684310002097)
Task(name=A, time=1684310003097)

2 DelayQueue源码分析

DelayQueue是基于PriorityQueue实现的,想了解DelayQueue的源码,需要先了解PriorityQueue

可以参考Java并发编程(十三)PriorityBlockingQueue

2.1 DelayQueue的核心属性

// 二叉堆,实际元素的存放位置
private final PriorityQueue<E> q = new PriorityQueue<E>();
// 锁
private final transient ReentrantLock lock = new ReentrantLock();  
// 配合锁使用,阻塞唤醒线程
private final Condition available = lock.newCondition();
// Leader-Follower线程模型
private Thread leader;

2.2 存数据

  • 所有存数据的方法最后调用的都是offer方法
public boolean offer(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        // 调用优先级队列,添加任务
        q.offer(e);
        // 拿到第一个数据,看看我是不是第一个,如果是第一个,可能有消费者挂起了,唤醒消费者
        if (q.peek() == e) {
            // 将leader更新为null,并唤醒等待线程
            leader = null;
            // condition.signal()唤醒线程
            available.signal();
        }
        // ok~返回true
        return true;
    } finally {
        lock.unlock();
    }
}

// 下面两个方法都是直接调用offer方法
public void put(E e) {  
    offer(e);  
}
public boolean offer(E e, long timeout, TimeUnit unit) {  
    return offer(e);  
}

2.3 取数据

  • poll,消费者尝试一下拿数据,如果有数据,并且延迟时间已经到了,返回,否则返回null
public E poll() {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        // 拿到堆顶数据
        E first = q.peek();
        // 如果没数据,或者数据的延迟时间没到,返回null
        if (first == null || first.getDelay(NANOSECONDS) > 0)
            return null;
        else
            // 如果有数据,并且时间到了,基于优先级队列,把任务取出来。  
            return q.poll();
    } finally {
        lock.unlock();
    }
}
  • poll(long timeout, TimeUnit unit),消费者尝试获取并等待至有数据可以取出或超时返回null
public E poll(long timeout, TimeUnit unit) throws InterruptedException {
    // 纳秒判断
    long nanos = unit.toNanos(timeout);
    final ReentrantLock lock = this.lock;
    // 这里加锁,允许中断。
    lock.lockInterruptibly();
    try {
        for (;;) {
            // 拿堆顶
            E first = q.peek();
            // 没数据,判断等多久
            if (first == null) {
                if (nanos <= 0)
                    // 时间没了,直接走
                    return null;
                else
                    // 时间还有,继续等
                    nanos = available.awaitNanos(nanos);
            } else {
                // 必然有数据!!!!
                // 取出堆顶数据的剩余时间
                long delay = first.getDelay(NANOSECONDS);
                // 如果时间已经到位了,直接调用优先级队列,把数据取出来
                if (delay <= 0)
                    return q.poll();
                // 再次判断等待时间
                if (nanos <= 0)
                    // 不等,告辞
                    return null;
                // 将临时变量置位null
                first = null; 
                // 如果剩余的等待时间,小于任务的延迟时间,或者已经有leader线程在等待了
                if (nanos < delay || leader != null)
                    nanos = available.awaitNanos(nanos);
                else {
                    // 我等待的时间内,必然可以拿到数据,并且没有leader
                    Thread thisThread = Thread.currentThread();
                    // 将当前线程置位leader,说明我是第一个在这等待数据的线程
                    leader = thisThread;
                    try {
                        // 当前线程先挂起,挂起任务剩余的延迟时间,会释放锁!
                        long timeLeft = available.awaitNanos(delay);
                        // 重新计算剩余的等待时间
                        nanos -= delay - timeLeft;
                    } finally {
                        // 将leader置位null
                        if (leader == thisThread)
                            leader = null;
                    }
                }
            }
        }
    } finally {
        // 如果leader为null,并且堆顶有数据,执行唤醒操作
        if (leader == null && q.peek() != null)
            available.signal();
        lock.unlock();
    }
}
  • 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 <= 0L)
                    // 时间已经到了,直接取出返回
                    return q.poll();
                // don't retain ref while waiting
                // 等待时不持有引用
                first = null; 
                if (leader != null)
                    // 已经有leader在等了,作为follower在这里等待
                    available.await();
                else {
                    // 没有leader在等,当前线程为leader
                    Thread thisThread = Thread.currentThread();
                    leader = thisThread;
                    try {
                        // leader只需要等待到延迟时间到了就行,然后执行finally中的代码,进入下一次循环
                        available.awaitNanos(delay);
                    } finally {
                        if (leader == thisThread)
                            leader = null;
                    }
                }
            }
        }
    } finally {
        if (leader == null && q.peek() != null)
            available.signal();
        lock.unlock();
    }
}