JAVA队列介绍(Queue)——DelayQueue(java延迟队列)

DelayQueue

DelayQueue是一个支持延时获取元素的无界阻塞队列

java实现原理

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;

    private final Condition available = lock.newCondition();
}

可以看到其内部维护了一个PriorityQueue对象,根据后续的方法源码解读中可以发现,此队列是DelayQueue最终保存元素的队列。因为PriorityQueue是一个无界的队列,所以DelayQueue自然也实现了无界队列。

基础操作

新增数据

类似其他的队列实现,其提供了add、put和offer三个方法进行元素的添加。

add和put

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

但是实际上根据源码可以发现,add和put方式内部调用的是offer方法。

offer

此方法是DelayQueue最终添加元素的最终逻辑。其内部主要使用的是PriorityQueue的方法

    public boolean offer(E e) {
     
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
     
            q.offer(e);
            // 当插入的元素为队首,证明之前没有数据
            // 此时可能存在阻塞的读取线程
            if (q.peek() == e) {
     
                // 当读取线程为空
                leader = null;
                // 唤醒相关线程
                available.signal();
            }
            return true;
        } finally {
     
            lock.unlock();
        }
    }

可以看到其入队的方法主要依托于PriorityQueue并没有其他特殊的内容。

取出/删除数据

上面介绍过DelayQueue是一个支持延迟获取元素的队列,而其提供的三个查询元素的方法中。peek只是实现了简单的数据查询,其延迟获取的方法主要在polltake

peek

    public E peek() {
     
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
     
            return q.peek();
        } finally {
     
            lock.unlock();
        }
    }

poll

   public E poll() {
     
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
     
            E first = q.peek();
            // first.getDelay(NANOSECONDS) 获取队首元素的超时时间
            if (first == null || first.getDelay(NANOSECONDS) > 0)
                return null;
            else
                return q.poll();
        } finally {
     
            lock.unlock();
        }
    }

    public E take() throws InterruptedException {
     
        final ReentrantLock lock = this.lock;
        // 使用lockInterruptibly方式获取锁
        lock.lockInterruptibly();
        try {
     
        // 进入循环
            for (;;) {
     
                E first = q.peek();
                // 如果为空则等待
                if (first == null)
                    available.await();
                else {
     
                    // first.getDelay(NANOSECONDS)
                    long delay = first.getDelay(NANOSECONDS);
                    // 超时则直接出队
                    if (delay <= 0)
                        return q.poll();
                        // 释放引用
                    first = null; // don't retain ref while waiting
                    // 此时当leader为空的时候会被当前线程赋值,
                    // 所以当leader不为空的时候,则代表有其他线程在操作内容
                    // 进行线程阻塞,这个时候因为leader只能有一个值,所以保证了只有一个线程线程去等待到时唤醒,避免大量唤醒操
                    if (leader != null)
                        available.await();
                    else {
     
                        Thread thisThread = Thread.currentThread();
                        // 赋值为当前线程
                        leader = thisThread;
                        try {
     
                            // 进行超时的阻塞
                            available.awaitNanos(delay);
                        } finally {
     
                            // 等待结束后释放leader
                            if (leader == thisThread)
                                leader = null;
                        }
                    }
                }
            }
        } finally {
     
            // leader为null并且队列不为空,说明没有其他线程在处理,且存在值
            // 则唤醒available的锁
            if (leader == null && q.peek() != null)
                available.signal();
            // 释放全局锁
            lock.unlock();
        }
    }

其出队流程可以描述为:

  1. 在循环中当不存在元素则使用条件Condition进行阻塞,让出资源。
  2. 存在数据的情况下,优先获取元素超时时间,假如已经超时则直接调用poll出队
  3. 不存在超时的情况下,首先判断,leader是否有值,有值则代表有其他线程正在操作,进行阻塞。
  4. leader无值得时候,表示没有线程正在等待元素,所以将leader进行赋值为本线程,然后根据之前计算的超时时间,进行指定超时时间的阻塞。当再次被唤醒后释放leader资源,然后获取元素。
  5. 当leader已经被释放,且队列不为空的时候,证明队列存在值且没有其他线程在处理,然后唤醒其他正在条件阻塞的线程。

这里需要关注的几个地方

 first = null; // don't retain ref while waiting
  1. 当线程1到达的时候,因为队首元素没有超时,则设置leader = 线程1。同时因为E first = q.peek();导致线程1持有队首元素引用。
  2. 当后续线程2到达的时候,因为leader = 线程1则会被阻塞,但是此时因为E first = q.peek();代码已经执行,导致其被线程2持有,假如线程阻塞完毕了,获取列首元素成功,出列。这个时候列首元素应该会被回收掉,但是问题是它还被线程2持有着,所以不会回收,就会造成内存泄漏。

Delayed

在出队的方法中我们看到方法使用Delayed进行超时判断。此接口是延迟队列进行延迟的主要逻辑。所以DelayQueue要求队列中的元素必须实现Delayed接口

public interface Delayed extends Comparable<Delayed> {
     
    long getDelay(TimeUnit unit);
}

该方法返回与此对象相关的的剩余时间。同时可以看到此接口继承了Comperable接口。所以实现Delayed接口的类存在一个排序逻辑,然后配合PriorityQueue队列可以保证在取出的元素的延时时间是有序的。

DelayQueue的使用

因为DelayQueue是基于Delayed的接口进行超时判断,所以元素需要继承此接口,下面一个简单使用此队列的例子

import java.util.concurrent.DelayQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;

/**
 * @author daify
 */
public class Test {
     

    public static void main(String[] args) throws InterruptedException {
     
        DelayQueue delayQueue = new DelayQueue();
        long l = System.currentTimeMillis();
        Item item = new Item(l+200,"item");
        Item item2 = new Item(l+100,"item2");
        Item item3 = new Item(l+150,"item3");
        Item item4 = new Item(l+300,"item4");
        delayQueue.add(item);
        delayQueue.add(item2);
        delayQueue.add(item3);
        delayQueue.add(item4);
        int size = delayQueue.size();

        long time = 0;

        for (int i = 0; i < size; i++) {
     
            long l1 = System.currentTimeMillis();
            System.out.println(l1 - time);
            time = l1;
            System.out.println(((Item)delayQueue.take()).getName());
        }

    }

}

class Item implements Delayed {
     

    private String name;

    private Long cancelTime;

    public Item(Long cancelTime,String name) {
     
        this.cancelTime = cancelTime;
        this.name = name;
    }

    @Override
    public long getDelay(TimeUnit unit) {
     
        long l = unit.convert(cancelTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
        return l;
    }

    @Override
    public int compareTo(Delayed o) {
     
        return this.getCancelTime().compareTo(((Item) o).getCancelTime());
    }

    public String getName() {
     
        return name;
    }

    public void setName(String name) {
     
        this.name = name;
    }

    public Long getCancelTime() {
     
        return cancelTime;
    }

    public void setCancelTime(Long cancelTime) {
     
        this.cancelTime = cancelTime;
    }
}

输出内容

item2
1000
item3
499
item
500
item4

此时可以看到在元素插入队列的时候,元素就已经根据时间进行重新排序了。所以取出的顺序是根据compareTo的结果计算的。

关于DelayQueue

延迟队列主要应用的场景:

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

个人水平有限,上面的内容可能存在没有描述清楚或者错误的地方,假如开发同学发现了,请及时告知,我会第一时间修改相关内容。假如我的这篇内容对你有任何帮助的话,麻烦给我点一个赞。你的点赞就是我前进的动力。

你可能感兴趣的:(JAVA,#,数据容器(集合),java延迟队列,DelayQueue,Queue)