Java延迟队列DelayQueue原理详解

 更新时间:2023年12月07日 09:38:59   作者:外星喵  
这篇文章主要介绍了Java延迟队列DelayQueue原理详解,DelayQueue 是一个通过PriorityBlockingQueue实现延迟获取元素的无界队列无界阻塞队列,其中添加进该队列的元素必须实现Delayed接口,而且只有在延迟期满后才能从中提取元素,需要的朋友可以参考下

什么是DelayQueue(延时队列)

DelayQueue 是一个通过PriorityBlockingQueue实现延迟获取元素的无界队列无界阻塞队列,其中添加进该队列的元素必须实现Delayed接口(指定延迟时间),而且只有在延迟期满后才能从中提取元素。

什么是PriorityBlockingQueue(优先队列)

PriorityBlockingQueue是一个支持优先级的无界阻塞队列,队列的元素默认情况下元素采用自然顺序升序排列,或者根据构造队列时提供的 Comparator 进行排序,具体取决于所使用的构造方法。

需要注意的是不能保证同优先级元素的顺序。

PriorityBlockingQueue也是基于最小二叉堆实现,使用基于CAS实现的自旋锁来控制队列的动态扩容,保证了扩容操作不会阻塞take操作的执行。

DelayQueue使用场景

DelayQueue可以运用在以下应用场景:

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

DelayQueue原理

DelayQueue的泛型参数需要实现Delayed接口,Delayed接口继承了Comparable接口,DelayQueue内部使用非线程安全的优先队列(PriorityQueue),并使用Leader/Followers模式,最小化不必要的等待时间。DelayQueue不允许包含null元素。

Leader/Followers模式:

有若干个线程(一般组成线程池)用来处理大量的事件

有一个线程作为领导者,等待事件的发生;其他的线程作为追随者,仅仅是睡眠。

假如有事件需要处理,领导者会从追随者中指定一个新的领导者,自己去处理事件。

唤醒的追随者作为新的领导者等待事件的发生。

处理事件的线程处理完毕以后,就会成为追随者的一员,直到被唤醒成为领导者。

假如需要处理的事件太多,而线程数量不够(能够动态创建线程处理另当别论),则有的事件可能会得不到处理。

所有线程会有三种身份中的一种:leader和follower,以及一个干活中的状态:proccesser。它的基本原则就是,永远最多只有一个leader。而所有follower都在等待成为leader。线程池启动时会自动产生一个Leader负责等待网络IO事件,当有一个事件产生时,Leader线程首先通知一个Follower线程将其提拔为新的Leader,然后自己就去干活了,去处理这个网络事件,处理完毕后加入Follower线程等待队列,等待下次成为Leader。这种方法可以增强CPU高速缓存相似性,及消除动态内存分配和线程间的数据交换。

DelayQueue源码解析

DelayQueue属性

//可重入同步锁
private final transient ReentrantLock lock = new ReentrantLock();
//DelayQueue的实现依赖于PriorityQueue(优先队列)
private final PriorityQueue<E> q = new PriorityQueue<E>();
//第一个等待某个延时对象的线程,在延时对象还没有到期时其他线程看到这个leader不为null,那么就直接wait
//主要是为了避免大量线程在同一时间点唤醒,导致大量的竞争,反而影响性能
private Thread leader = null;
//条件队列,用于wait线程
private final Condition available = lock.newCondition();

DelayQueue构造方法

    //从上面属性就可以看出,DelayQueue采用了饿汉模式,调用构造方法即创建了队列实例
    public DelayQueue() {}
    /**
     * 创建一个DelayQueue,最初包含给定的Collection实例集合。
     * @param c 最初包含的元素集合
     */
    public DelayQueue(Collection<? extends E> c) {
        this.addAll(c);
    }

DelayQueue主要方法

offer添加元素

public boolean offer(E e) {
    final ReentrantLock lock = this.lock;
    lock.lock();
    try {
        //调用优先队列
        q.offer(e);
        //检验元素是否为队首,是则设置 leader 为null, 并唤醒一个消费线程  
        if (q.peek() == e) {
            leader = null;
            available.signal();
        }
        return true;
    } finally {
        lock.unlock();
    }
}

take获取元素

public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
        for (;;) {
            //从优先队列中获取第一个元素,peek方法不会删除元素
            E first = q.peek();
            //如果获取不到数据,则调用available.await()进入阻塞状态
            if (first == null)
                available.await();
            else {
                //获取当前延时对象是否到期
                long delay = first.getDelay(NANOSECONDS);
                //到期那么返回这个延时对象
                if (delay <= 0)
                    return q.poll();
                first = null; // 
                //leader不为空,表明已经有其他线程在等待这个延时对象了
                //为什么不available.awaitNanos(delay)呢?这将会导致大量的线程在同一时间点被唤醒,然后去竞争
                //这个到期的延时任务,影响性能,还不如直接将他们无时间限制的wait,leader线程或者其他新进来的线程获取到延时对象后,去唤醒
                //让他们去竞争下一个延时对象
                if (leader != null)
                    available.await();
                else {
                    Thread thisThread = Thread.currentThread();
                    leader = thisThread;
                    try {
                        //指定纳秒级别线程阻塞时间,当前wait住的线程被唤醒后有可能与其他线程竞争失败,就会进入了同步队列阻塞,那个抢到锁的线程就会取走这个延时对象
                        available.awaitNanos(delay);
                    } finally {
                        //leader线程被唤醒并获取到锁之后会将leader设置为空
                        if (leader == thisThread)
                            leader = null;
                    }
                }
            }
        }
    } finally {
        //leader为空并且队列不为空,那么唤醒正在等待的线程
        if (leader == null && q.peek() != null)
            available.signal();
        lock.unlock();  //释放锁
    }
}

从优先队列中取值,如果取到的延时节点已经已经到期,那么直接返回,如果还没有到期并且已经有其他线程在执行delay时间等待了(也就是leader线程),那么挂起自己(避免延时 相同时间造成大量线程同时唤醒), leader线程在指定delay时间后主动唤醒,然后取竞争锁,如果竞争成功,那么很大概率可以获取到延时节点,如果竞争失败,将被阻塞。

remove删除元素

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

Delayed接口

使用DelayQueue的话,放入该队列的对象必须实现Delayed接口,实现的接口中有两个参数:延迟时间单位,优先级规则,take方法会根据规则按照优先级执行

Delayed接口源码:

public interface Delayed extends Comparable<Delayed> {
    /**
     * 返回与此对象关联的剩余延迟(给定的时间单位)。
     * @param unit 时间单位
     * @返回剩余延迟;零值或负值表示 延迟已过期
     */
    long getDelay(TimeUnit unit);
}

因为Delayed继承了Comparable,所以还需要实现compareTo方法,具体实现如下:

class MyDelay implements Delayed {
    long delayTime; // 延迟时间
    long expire; // 过期时间
    public MyDelay(long delayTime, Thread t) {
        this.delayTime = delayTime;
        // 过期时间 = 当前时间 + 延迟时间
        this.expire = System.currentTimeMillis() + delayTime;
    }
    /**
     * 剩余时间 = 到期时间 - 当前时间
     */
    @Override
    public long getDelay(TimeUnit unit) {
        return unit.convert(this.expire - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
    }
    /**
     * 优先级规则:两个任务比较,时间短的优先执行
     */
    @Override
    public int compareTo(Delayed o) {
        long f = this.getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS);
        return (int) f;
    }
}

使用示例

实现Delayed接口:

class MyDelay<T> implements Delayed {
    long delayTime; // 延迟时间
    long expire; // 过期时间
    T data;
    public MyDelay(long delayTime, T t) {
        this.delayTime = delayTime;
        // 过期时间 = 当前时间 + 延迟时间
        this.expire = System.currentTimeMillis() + delayTime;
        data = t;
    }
    /**
     * 剩余时间 = 到期时间 - 当前时间
     */
    @Override
    public long getDelay(TimeUnit unit) {
        return unit.convert(this.expire - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
    }
    /**
     * 优先级规则:两个任务比较,时间短的优先执行
     */
    @Override
    public int compareTo(Delayed o) {
        long f = this.getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS);
        return (int) f;
    }
    @Override
    public String toString() {
        return "delayTime=" + delayTime +
                ", expire=" + expire +
                ", data=" + data;
    }
}

测试用例如下:

public class DelayQueueDemo {
    static BlockingQueue<Delayed> queue = new DelayQueue();
    public static void main(String[] args) throws InterruptedException {
        queue.add(new MyDelay(8, "第一次添加任务"));
        queue.add(new MyDelay(3, "第二次添加任务"));
        queue.add(new MyDelay(5, "第三次添加任务"));
        while (!queue.isEmpty()) {
            Delayed delayed = queue.take();
            System.out.println(delayed);
        }
    }
}

输出如下:

delayTime=3, expire=1625902338874, data=第二次添加任务
delayTime=5, expire=1625902338876, data=第三次添加任务
delayTime=8, expire=1625902338879, data=第一次添加任务

总结

DelayQueue其实采用了装饰器模式,在对PriorityQueue进行包装下增加了延时时间获取元素的功能,其主要特点归纳如下:

  1. DelayQueue是一个无界阻塞队列,队列内部使用PriorityQueue来实现。
  2. 进入队列的元素必须实现Delayed接口,在创建元素时可以指定多久才能从队列中获取当前元素,只有在延迟期满时才能从中提取元素;
  3. 该队列头部是延迟期满后保存时间最长的Delayed元素;
  4. 如果没有延迟未过期元素,且队列没有头部,并且poll将返回null;
  5. 当一个元素的getDelay(TimeUnit.NANOSECONDS)方法返回一个小于等于0的值时,表示该元素已过期;
  6. 无法使用poll或take移除未到期的元素,也不会将这些元素作为正常元素对待;例如:size方法返回到期和未到期元素的计数之和。
  7. 此队列不允许使用null元素。

到此这篇关于Java延迟队列DelayQueue原理详解的文章就介绍到这了,更多相关延迟队列DelayQueue原理内容请搜索脚本之家以前的文章或继续浏览下面的相关文章希望大家以后多多支持脚本之家!

相关文章

最新评论