【延时任务】-DelayQueue详解

 

 

DelayQueue

1. 使用方法

DelayQueue中的元素要实现Delayed接口,Delayed接口又实现了 Comparable接口

public interface Delayed extends Comparable<Delayed> {
    //用于返回还有多久到期
    long getDelay(TimeUnit unit);
}

public interface Comparable<T> {
    //用于入队元素的排序,对于延时队列一般让过期时间短的放在前面
    public int compareTo(T o);
}
class Work implements Delayed{

    String name;
    //过期时间
    long time;

    public Work(String name, long time, TimeUnit timeUnit) {
        this.name = name;
        //计算到期时间
        this.time = timeUnit.toMillis(time)+System.currentTimeMillis();
    }

    @Override
    public long getDelay(TimeUnit unit) {
        //根据给的时间单位返回剩余到期时间,注意time的保存单位,根据time选择合适的TimeUnit
        return unit.convert(time-System.currentTimeMillis(),TimeUnit.MILLISECONDS);
    }

    @Override
    public int compareTo(Delayed o) {
        //根据到期时间从小到大排序
        return Long.compare(this.time,o.getDelay(TimeUnit.MILLISECONDS));
    }
}

public class TestMain {
    public static void main(String[] args) throws InterruptedException {
    
        DelayQueue<Work> q=new DelayQueue<>();
        q.add(new Work("001",10,TimeUnit.SECONDS));
        q.add(new Work("002",15,TimeUnit.SECONDS));
        q.add(new Work("003",20,TimeUnit.SECONDS));

        System.out.println("begin time:" + new Date()); 
        for (int i = 0; !q.isEmpty(); i++) {
            Work take = q.take();
            if(take == null){ 
                continue;
            }
            System.out.format("name:{%s}, time:{%s}\n",take.name, new Date());
        } 
    }
}

2. 源码解析

我们根据上面的调用示例来看看源码,先看看入队操作

入队

通过源码我们可以看到DelayQueue一共有4个入队方法,add(E),offer(E),put(E),offer(T,long,TimeUnit),但其实都是调用offer(E)的实现。

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

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

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

    public boolean offer(E e) {
        /**
         * this.lock 源码为
         * private final transient ReentrantLock lock = new ReentrantLock();
         * 可重入锁
         */
        final ReentrantLock lock = this.lock;
        // 获取锁
        lock.lock();
        try {
            /**
            * q 的源码为
             * private final PriorityQueue<E> q = new PriorityQueue<E>();
             * 一个基于优先级的无界优先级队列
             */
            // 把元素入队并保持顺序
            q.offer(e);
   
            //如果新入元素在队头
            if (q.peek() == e) {
                /**
                 * leader 源码为
                 * private Thread leader = null;
                 * leader为当前正在等待队首元素的线程
                 */
                // 把leader设为空,因为队首有更先出队的元素,之前的leader等待的就不是最先出队的元素了,
                leader = null;
                /**
                 * avaiable 源码为
                 * private final Condition available = lock.newCondition();
                 * availale表示某种条件,可以唤醒或阻塞线程
                 */
                //唤醒正在等待此条件线程中的一个,这个线程必须重新再获得锁
                available.signal();
            }
            return true;
        } finally {
            lock.unlock();
        }
    }

出队

出队也有4个方法,poll(),take(),poll(long,TimeUnit),peek() ,我这边主要写一下poll和take

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();
        }
    }

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的引用,
                    // 因为在线程等待期间不释放引用,
                    // 多线程情况下这个元素可能会被其他线程弹出,而本线程还未释放引用造成内存泄露
                    first = null; // don't retain ref while waiting
                    // 如果已经有线程在等待头元素了,则本线程等待
                    if (leader != null)
                        available.await();
                    else {
                        Thread thisThread = Thread.currentThread();
                        // 没有线程在等待头元素,则把本线程设为等待头元素线程
                        leader = thisThread;
                        try {
       // 本地线程等待到 头元素 到期时间自动唤醒
                            available.awaitNanos(delay);
                        } finally { 
                            if (leader == thisThread)
                                leader = null;
                        }
                    }
                }
            }
        } finally {
            if (leader == null && q.peek() != null)
                available.signal();
            lock.unlock();
        }
    }

3. 总结

  • DelayQueue通过全局可重入锁来实现同步
  • DelayQueue常用于定时任务
  • DelayQueue内部使用优先级队列来存储
posted @ 2020-07-29 16:10  A_yes  阅读(667)  评论(0编辑  收藏  举报