延时队列DalayQueue的源码分析

延时队列DalayQueue的源码分析

一、前言

DalayQueue是Java并发包中的一个队列,是优先级队列实现的无界阻塞队列,它提供了一种先进先出(FIFO)的数据结构,并且允许在元素达到期望的时间后获取或删除它们。下图展示了DalayQueue的继承、接口及属性关系:

在这里插入图片描述

​ 下面是DelayQueue类的主要成员:

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

​ 如上图所示DelayQueue实现了阻塞队列BlockingQueueDelayQueue中内部使用的是PriorityQueue存放数据,其元素实现了Delayed接口,而Delayed继承了Comparable接口,compareTo方法用于元素比较; getDelay方法用于获取元素剩余延时时间,以实现PriorityQueue内元素排序。使用ReentrantLock实现线程同步,使用Condition实现延时等待。使用变量leader来判断线程占有情况。

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

public interface Comparable<T> {
    public int compareTo(T o);
}

二、源码分析

2.1 新增元素

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

    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();
        }
    }
    
    public void put(E e) {
        offer(e);
    }

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

​ 可以看出不管是add还是put都是调用offer来进行添加元素的,这里通过ReentrantLock来获取锁,如果队列首部为当前元素,则设置leadernull,并唤醒等待线程。

2.2 获取元素

2.2.1 poll函数

​ 只取过期的元素,当队列中没有过期元素时,会返回null

	public E poll() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            E first = q.peek();
            if (first == null || first.getDelay(NANOSECONDS) > 0)
                return null;
            else
                return q.poll();
        } finally {
            lock.unlock();
        }
    }
2.2.2 take函数

​ 在获取元素时,只能一个线程获取,如果队首元素已过期,则取出;如果队首元素没有过期,看leader是否为null,不为null则说明是其他线程也在执行take()则把当前线程放入条件队列,否则就是只有当前线程在执行take()方法,则当前线程await直到剩余过期时间到,这期间该线程会释放锁,所以其他线程可以offer()添加元素,也可以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 = null;
                    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();
        }
    }
2.2.3 drainTo函数

​ 将超时的元素排放到集合中,返回获取数量。

	public int drainTo(Collection<? super E> c) {
        if (c == null)
            throw new NullPointerException();
        if (c == this)
            throw new IllegalArgumentException();
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            int n = 0;
            // 获取超时的元素
            for (E e; (e = peekExpired()) != null;) {
                c.add(e);
                q.poll();
                ++n;
            }
            return n;
        } finally {
            lock.unlock();
        }
    }

三、优缺点

  • 优点
    1. 简单易用
    2. 自动排序
    3. 线程安全
    4. 内部元素有”延迟”特性:只有延迟到期的元素才允许被获取
  • 缺点
    1. 如果队列中的元素数量较多,那么获取元素时的效率会较低,因为每次出队操作都需要对队列进行排序。
    2. 它的容量是无界的,如果队列中的元素数量非常大,可能会消耗大量内存,甚至导致内存溢出。
    3. 数据无持久化机制,系统宕机时会出现数据丢失。

四、改造思路

  1. 可以考虑使用redis来进行持久化,使用zset来进行时间排序。优先队列的peek可用zsetzrange替代,poll可用zpopmin替代。可解决缺点1/3问题。当然除了使用redis也可以用其他的数据库来进行替代,这里只是提出一种方案。
  2. 可以考虑在offer时进行队列大小判断,在超出一定大小时抛出异常。可解决缺点2问题。
  • 21
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值