深度解析延迟队列DelayQueue

前言

有时候,我们有一些任务需要“稍后”来做,比如一些连接需要空闲一段时间后再关闭,session需要空闲一段时间后自动退出。这个时候就需要一些可以延迟执行任务的工具。DelayQueue(延迟队列)就是一个可以实现类似功能的工具。

由于本篇会涉及到优先队列PriorityQueue,所以预先阅读 深度解析优先级队列PriorityQueue 很有必要。

DelayQueue

DelayQueue(延迟队列)的标准实现出现在JDK1.5中的J.U.C包中,作为一个工具类,用来管理一些需要延迟处理的任务。先来看下它的类体系结构:
DelayQueue
需要注意的是DelayQueue实现了BlockingQueue接口,意味着它也是阻塞队列,但是类名称中没有包含Blocking

示例演示

因为DelayQueue比一般队列使用起来稍微复杂一定,所以先看一个实例:

/**
 * @author sicimike
 */
public class DelayQueueDemo {

    public static void main(String[] args) {
        final long currentTime = System.currentTimeMillis();

        DelayQueue<DelayObject> queue = new DelayQueue();
        queue.offer(new DelayObject("Sic-001", currentTime + 6000L));
        queue.offer(new DelayObject("Sic-002", currentTime + 3000L));
        queue.offer(new DelayObject("Sic-003", currentTime + 8000L));

        System.out.println(System.currentTimeMillis() + " -> 入队完成" );

        while (!queue.isEmpty()) {
            try {
                DelayObject delayObject = queue.take();
                System.out.println(System.currentTimeMillis() + " -> " + delayObject);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }
}

其中队列内部元素DelayObject定义如下:

/**
 * 延迟队列中的元素
 * 必须实现Delayed接口
 * @author sicimike
 */
class DelayObject implements Delayed {

    // 过期时间
    private Long destroyTime;
    private String id;

    @Override
    public long getDelay(TimeUnit unit) {
    	// 返回元素剩余的时间
        return unit.convert(destroyTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
    }

    @Override
    public int compareTo(Delayed o) {
    	// 剩余时间少的,排在队列前面
        return (int) (this.getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS));
    }

    public DelayObject() {
    }

    public DelayObject(String id, Long destroyTime) {
        this.id = id;
        this.destroyTime = destroyTime;
    }

    public Long getDestroyTime() {
        return destroyTime;
    }

    public void setDestroyTime(Long destroyTime) {
        this.destroyTime = destroyTime;
    }

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    @Override
    public String toString() {
        return "DelayObject {" +
                "destroyTime=" + destroyTime +
                ", id='" + id + '\'' +
                '}';
    }

}

执行结果:

1576763926966 -> 入队完成
1576763929959 -> DelayObject {destroyTime=1576763929959, id='Sic-002'}
1576763932959 -> DelayObject {destroyTime=1576763932959, id='Sic-001'}
1576763934961 -> DelayObject {destroyTime=1576763934959, id='Sic-003'}

从执行结果可以看出,三个元素入队完成后,大概延迟了3秒,出队了第一个元素;继续延迟3秒,出队第二个元素;继续延迟2秒,出队第三个元素。
元素的出队顺序与放入的顺序无关,而与元素延迟的时间有关,按照升序排列出队,顺序由DelayObject中的compareTo方法决定。
放入DelayQueue中的元素必须实现Delayed接口,实现接口中的getDelay方法和compareTo方法。getDelay获取的是元素剩余的时间,所以不同的时刻调用该方法,应该得到不同的值,否则元素永远不会过期。compareTo方法用于给元素排序,排在前面的先出队。
这就是延迟队列DelayQueue的基本使用。

内部结构

大致了解DelayQueue的使用后,再来看下DelayQueue是如何实现的。首先就要看它的类定义和成员变量

public class DelayQueue<E extends Delayed> extends AbstractQueue<E>
    implements BlockingQueue<E> {

	// 可重入锁
    private final transient ReentrantLock lock = new ReentrantLock();
    
    // 优先队列
    private final PriorityQueue<E> q = new PriorityQueue<E>();
    
    // 等待队列头元素的线程
    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();
}

根据定义可以看出底层通过组合的方式持有优先队列PriorityQueue的对象,也就是说延迟队列DelayQueue底层是通过优先队列PriorityQueue来实现的。

核心方法

需要重点关注的核心操作只有三个:入队出队查看队首元素

offer(E e)方法

offer(E e)方法用于往DelayQueue中放入元素。由于DelayQueue是无界队列(底层PriorityQueue可以自动扩容),所以往DelayQueue中放入元素是非阻塞的。putadd均是调用offer方法,其实现如下:

扫描二维码关注公众号,回复: 8985809 查看本文章
public void put(E e) {
    offer(e);
}

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

public boolean offer(E e, long timeout, TimeUnit unit) {
    return offer(e);
}

// 真正完成入队操作的方法
public boolean offer(E e) {
    final ReentrantLock lock = this.lock;
    // 加锁
    lock.lock();
    try {
    	// 入队(直接调用PriorityQueue的入队方法)
        q.offer(e);
        if (q.peek() == e) {
        	// 放入的元素恰好是下一个要出队的元素
        	// 唤醒因调用take方法而被阻塞的线程
        	// leader设置为null是为了使当前队首元素快速进入设置了时间的阻塞状态(结合take方法)
            leader = null;
            available.signal();
        }
        return true;
    } finally {
    	// 解锁
        lock.unlock();
    }
}

take()方法

take()方法以阻塞的方式从DelayQueue中取出一个元素。

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; // don't retain ref while waiting
                if (leader != null)
                	// leader不为null,表示有线程已经设置了阻塞时间,当前线程直接被阻塞
                    available.await();
                else {
                	// 没有线程设置过阻塞时间(或者阻塞时间已经到了)
                    Thread thisThread = Thread.currentThread();
                    // 当前线程设置成leader线程
                    leader = thisThread;
                    try {
                    	// 因为队首元素还有delay时间才能出队,所以休眠delay时间
                        available.awaitNanos(delay);
                    } finally {
                        if (leader == thisThread)
                        	// 休眠时间到了,释放leader
                        	// 下一次循环出队
                            leader = null;
                    }
                }
            }
        }
    } finally {
        if (leader == null && q.peek() != null)
        	// 前一个元素已经出队,并且队列中还有元素
        	// 唤醒其他被阻塞的线程
            available.signal();
        // 解锁
        lock.unlock();
    }
}

通过take方法可以看出DelayQueue使用ReentrantLock + Condition来实现线程的阻塞、唤醒。再加上循环,如果队首元素不出队,线程会一直被阻塞。
这里的leader使用的是Leader/Follower模型,队首元素尚未出队时,leader不为null。其余的线程知道leader不为null之后就被无限阻塞;队首元素出队后,leader为null,通知其余的线程来争抢。

总结

DelayQueue实现了BlockingQueue接口,是阻塞队列。底层利用了优先队列PriorityQueue的自动扩容、排序等机制。再利用ReentrantLock + Condition 机制实现线程安全,以及线程休眠、唤醒。

发布了52 篇原创文章 · 获赞 107 · 访问量 1万+

猜你喜欢

转载自blog.csdn.net/Baisitao_/article/details/103606151