阻塞队列之DelayQueue源码分析

阻塞队列之DelayQueue

上一篇文章笔主介绍了BlockingQueue的实现类之一,即ArrayBlockingQueue,了解了其内部的具体实现。今天,我们再来学习下BlockingQueue的另一个实现类DelayQueue。

DelayQueue的定义

我们看下JDK中对DelayQueue的定义,如下:

An unbounded {@linkplain BlockingQueue blocking queue} of Delayed elements, in which an element can only be taken when its delay has expired. The head of the queue is that Delayed element whose delay expired furthest in the past. If no delay has expired there is no head and poll will return null. Expiration occurs when an element’s getDelay(TimeUnit.NANOSECONDS) method returns a value less than or equal to zero. Even though unexpired elements cannot be removed using take or poll, they are otherwise treated as normal elements. For example, the size method returns the count of both expired and unexpired elements. This queue does not permit null elements.

DelayQueue是一个内部元素为Delayed类型的无界阻塞队列,其中的元素只有在延迟到期时才可以被移除。队列头部的元素在过去的延迟过期时间最久。如果没有延迟过期的元素,队列就没有头部元素,并且队列的poll操作也会返回null。当元素的getDelay(timeUnit.Nanoseconds)返回值小于或等于零时,就会发生过期。即使无法使用take或poll删除未过期的元素,它们也会被视为正常的元素。例如,size方法的返回值就是过期和未过期元素个数之和。队列不允许插入空元素。

DelayQueue类的特点

简单总结下,DelayQueue有以下特点:

  • DelayQueue是一个无界队列,没有容量限制。
  • 队列内部的元素必须是Delayed类型,也就是元素必须实现Delayed接口。
  • 队列头部只有在延迟到期时才可以被移除,否则,移除操作会返回null。

在分析DelayQueue的源码之前,我们必须先看下Delayed类的定义。这是为什么呢?原因很简单,因为DelayQueue规定内部元素必须是Delayed类型。

什么是Delayed

Delayed是一个interface,定义也比较简单,Delayed的源码如下:

**
 * A mix-in style interface for marking objects that should be
 * acted upon after a given delay.
 *  * <p>An implementation of this interface must define a
 * {@code compareTo} method that provides an ordering consistent with
 * its {@code getDelay} method.
 *  * @since 1.5
 * @author Doug Lea
 */
public interface Delayed extends Comparable<Delayed> {

    /**
     * Returns the remaining delay associated with this object, in the
     * given time unit.
     * @param unit the time unit
     * @return the remaining delay; zero or negative values indicate
     * that the delay has already elapsed
     */
    long getDelay(TimeUnit unit);
}

Delayed是一种混合样式接口,用于标记那些在给定延迟后应该有所行动的对象。此接口的实现类必须定义compareTo方法,该方法与其getDelay方法提供一致的顺序。从源码来看,Delayed继承了Comparable< Delayed >接口,因此实现了Delayed接口的类需要重写getDelay方法,同时需要重写compareTo方法,来达到根据延迟过期时间排序的目的。

了解了Delayed接口,我们重新回到正文,来分析DelayQueue的源码。

DelayQueue类的成员属性

DelayQueue主要定义了以下成员属性,源码如下:

    //所有对队列的操作都需要使用此锁
    private final transient ReentrantLock lock = new ReentrantLock();
   
    //DelayQueue内部元素的存储实际使用了优先级队列PriorityQueue
    private final PriorityQueue<E> q = new PriorityQueue<E>();

    //当前获取到锁的消费者线程
    private Thread leader = null;

    //依赖于ReentrantLock对象(lock)的Condition对象(available ) 
    private final Condition available = lock.newCondition();

DelayQueue的成员属性不多,但是都非常关键。

  • lock属性,队列元素的插入、移除、查询、清空、删除等操作都需要使用此锁。
  • q属性,DelayQueue内部并没有实现队列元素的存储,而是使用了优先级队列PriorityQueue。
  • leader属性,表示了当前获取到锁的消费者线程,由此属性可以有效减少多个消费者线程竞争资源时的等待时间。
  • available属性,能够及时唤醒插入或者移除操作阻塞的线程。
DelayQueue类的构造方法

DelayQueue提供了两种方式来构造阻塞队列,分别是:

  1. 默认的无参构造方法
  2. 将参数给定集合中的元素依次插入队列中的构造方法

构造方法源码如下:

    //无参构造方法
    public DelayQueue() {}

    //将给定集合中的元素依次插入到队列中
    public DelayQueue(Collection<? extends E> c) {
        this.addAll(c);
    }
DelayQueue类的成员方法

DelayQueue类的成员方法可以划分为以下几类:

  • 公共方法
  • 插入方法
  • 移除方法

这里公共方法是指在类内部会被多次调用的方法或者是获取队列属性的方法,例如:查询队里头部元素的方法、获取队列元素个数的方法、清空整个队列的方法、将队列转换为数组的方法等等。源码如下:

   //查询队列头部的元素
   public E peek() {
        final ReentrantLock lock = this.lock;
        lock.lock();  //获取锁
        try {
            //调用优先级队列的查询队列头部元素的方法
            return q.peek(); 
        } finally {
            lock.unlock();  //释放锁
        }
    }

    //查询队列中的元素个数
    public int size() {
        final ReentrantLock lock = this.lock;
        lock.lock();  //获取锁
        try {
            //调用优先级队列的size()
            return q.size();
        } finally {
            lock.unlock();  //释放锁
        }
    }

    //查询队列中延迟到期的元素
    private E peekExpired() {
        //调用优先级队列的peek(),获取数组的第一个元素
        E first = q.peek();
        //如果头部元素为空或者头部元素未过期返回null,否则,返回延迟到期的元素
        return (first == null || first.getDelay(NANOSECONDS) > 0) ?
            null : first;
    }
  
   //将队列中延迟到期的元素移除并放入集合
   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();  //释放锁
        }
    }

    //将队列中延迟到期的元素移除并放入集合,同时限制移除的元素个数
    public int drainTo(Collection<? super E> c, int maxElements) {
        if (c == null)
            throw new NullPointerException();
        if (c == this)
            throw new IllegalArgumentException();
        if (maxElements <= 0)
            return 0;
        final ReentrantLock lock = this.lock;
        lock.lock();  //获取锁
        try {
            int n = 0;
            for (E e; n < maxElements && (e = peekExpired()) != null;) {  //获取延迟过期的元素同时未超过指定移除的元素个数
                c.add(e);       //将延迟过期的元素加入集合
                q.poll();         //将延迟过期的元素从集合移除
                ++n;
            }
            return n;
        } finally {
            lock.unlock();   //释放锁
        }
    }

    //清空队列
    public void clear() {
        final ReentrantLock lock = this.lock;
        lock.lock();   //获取锁
        try {
            q.clear();    //调用优先级队列清空队列的方法
        } finally {
            lock.unlock();  //释放锁
        }
    }

    //返回队列还可以容纳的元素个数,总是返回Integer.MAX_VALUE,因为队列无界
    public int remainingCapacity() {
        return Integer.MAX_VALUE;
    }

    //将队列转换为Object数组
    public Object[] toArray() {
        final ReentrantLock lock = this.lock;
        lock.lock();   //获取锁
        try {
            return q.toArray();   //调用优先级队列的toArray()
        } finally {
            lock.unlock();   //释放锁
        }
    }

    //将队列转换为T类型数组
    public <T> T[] toArray(T[] a) {
        final ReentrantLock lock = this.lock;
        lock.lock();  //获取锁
        try {
            return q.toArray(a);   //调用优先级队列的toArray(a)
        } finally {
            lock.unlock();   //释放锁
        }
    }

   //返回队列的迭代器对象
   public Iterator<E> iterator() {
        return new Itr(toArray());
    }

DelayQueue插入操作提供了add(E e)、put(E e)、offer(E e, long timeout, TimeUnit unit)、offer(E e)这四个方法,由于DelayQueue是无界队列,所以add(E e)、put(E e)、offer(E e, long timeout, TimeUnit unit)这三个方法内部都是调用了offer(E e)。这四个方法的源码如下:

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

    //向队列中插入元素的方法
    public boolean offer(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();  //获取锁
        try {
            q.offer(e);  //调用优先级队列的offer(e)
            //如果查询队列头部的元素正好等于e,就唤醒移除操作阻塞的线程
            if (q.peek() == e) {    
                leader = null;  //e将会是最先延迟过期的元素,将获取到锁的消费者线程置为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);
    }

DelayQueue移除操作提供了 take()、poll(long timeout, TimeUnit unit)、poll()、remove(Object o) 、removeEQ(Object o)这五个方法,这五个方法的特点如下:

  1. take():如果队列为空,移除操作会阻塞直至队列中有元素。
  2. poll(long timeout, TimeUnit unit):如果队列为空,移除操作在给定的时间会阻塞。如果阻塞结束队列还是为空,返回false。否则,从队列的头部移除元素。
  3. poll():如果队列为空,直接返回null。否则,从队列的头部移除元素。
  4. remove(Object o) :remove(Object o)并非是从队列的头部移除元素,而是根据给定的元素o在队列中查找。如果找到元素o,就将元素o从队列中删除,此方法寻找元素o是使用下标遍历的方式。
  5. removeEQ(Object o)::removeEQ(Object o)并非是从队列的头部移除元素,而是根据给定的元素o在队列中查找。如果找到元素o,就将元素o从队列中删除,此方法寻找元素o是使用迭代器遍历的方式。

这五个方法的源码如下:

    /**
    * 如果队列为空或者队列头部元素未延迟过期,直接返回null。
    * 否则,从队列的头部移除元素。
    */
    public E poll() {
        final ReentrantLock lock = this.lock;
        lock.lock();   //获取锁
        try {
            E first = q.peek();  //调用优先级队列的peek(),获取数组的第一个元素
            if (first == null || first.getDelay(NANOSECONDS) > 0)
                return null;
            else
                return q.poll();   //调用优先级队列的poll()
        } finally {
            lock.unlock();  //释放锁
        }
    }

    /**
    * 如果队列为空或者队列头部元素未延迟过期,移除操作会阻塞。
    */
    public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();  //获取锁
        try {
            for (;;) {
                E first = q.peek();   //调用优先级队列的peek(),获取数组的第一个元素
                if (first == null)   //first为null,说明队列为空
                    available.await();  //阻塞当前线程移除操作
                else {
                    long delay = first.getDelay(NANOSECONDS);
                    if (delay <= 0)  //如果队列头部元素延迟已过期
                        return q.poll();  //调用优先级队列的poll()移除头部元素
                    // 放弃对队列头部元素的引用,防止队列头部元素被其他线程移除时依然有引用指向它,导致移除的元素不能被GC出现内存溢出   
                    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();  //释放锁
        }
    }

     /**
    * 如果队列为空或者队列头部元素未延迟过期,移除操作在给定的时间会阻塞。
    */
    public E poll(long timeout, TimeUnit unit) throws InterruptedException {
        long nanos = unit.toNanos(timeout);
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();  //获取锁
        try {
            for (;;) {
                E first = q.peek();  //调用优先级队列的peek(),获取数组的第一个元素
                if (first == null) {
                    if (nanos <= 0)
                        return null;  //阻塞结束队列依然为空,返回null
                    else
                        nanos = available.awaitNanos(nanos); //阻塞当前移除操作一段时间
                } else {
                    long delay = first.getDelay(NANOSECONDS);
                    if (delay <= 0)  //如果队列头部元素延迟已过期
                        return q.poll();   //调用优先级队列的poll()
                    if (nanos <= 0)  //阻塞结束队列头部元素未延迟过期,返回null
                        return null;
                     // 放弃对队列头部元素的引用,防止队列头部元素被其他线程移除时依然有引用指向它,导致移除的元素不能被GC出现内存溢出
                    first = null; 
                    if (nanos < delay || leader != null)  //继续阻塞当前移除操作
                        nanos = available.awaitNanos(nanos);
                    else {
                        Thread thisThread = Thread.currentThread();
                        leader = thisThread;
                        try {
                            //计算出最小的阻塞时间
                            long timeLeft = available.awaitNanos(delay);
                            nanos -= delay - timeLeft;
                        } finally {
                            if (leader == thisThread)
                                leader = null;
                        }
                    }
                }
            }
        } finally {
         //如果没有消费线程获取到锁同时队列不为空
            if (leader == null && q.peek() != null)
                  //唤醒阻塞的消费线程
                available.signal();
            lock.unlock();  //释放锁
        }
    }

   //根据给定的参数从队列中删除元素
   public boolean remove(Object o) {
        final ReentrantLock lock = this.lock;
        lock.lock();  //获取锁
        try {
            return q.remove(o);  //调用优先级队列的remove(o)
        } finally {
            lock.unlock();  //释放锁
        }
    }

    //根据给定的参数从队列中删除元素
    void removeEQ(Object o) {
        final ReentrantLock lock = this.lock;
        lock.lock();  //获取锁
        try {
            for (Iterator<E> it = q.iterator(); it.hasNext(); ) {
                if (o == it.next()) {
                    it.remove();   //调用迭代器的remove(o)
                    break;
                }
            }
        } finally {
            lock.unlock();  //释放锁
        }
    }

分析完源码,顺便写个demo来看下如何使用DelayQueue。

DelayQueue的使用

上文中提到,DelayQueue中的元素必须是Delayed类型,也就是元素必须实现Delayed接口。这里我们定义一个延迟消息类DelayMessage,使其实现Delayed接口,代码如下:

/**
 * 延迟消息类
 * @author zhaoheng
 * @date 2019年5月26日
 */
public class DelayMessage<K, V> implements Delayed {

	/**
	 * 消息的键
	 */
	private K key;

	/**
	 * 消息的值
	 */
	private V value;

	/**
	 * 延迟时间
	 */
	private Date delayDate;

	public DelayMessage(K key, V value, Date delayDate) {
		this.key = key;
		this.value = value;
		this.delayDate = delayDate;
	}

	/**
	 * 获取当前对象的延迟过期时间
	 * @param unit 时间单位
	 */
	@Override
	public long getDelay(TimeUnit unit) {
		long diff = delayDate.getTime() - new Date().getTime();
		return unit.convert(diff, TimeUnit.MILLISECONDS);
	}

	/**
	 * 重写compareTo()便于向队列中插入元素时,队列头部的元素总是延迟时间最先到达的
	 * @param o
	 */
	@Override
	public int compareTo(Delayed o) {
		long result = this.getDelay(TimeUnit.NANOSECONDS) - o.getDelay(TimeUnit.NANOSECONDS);
		if (result < 0) {
			return -1;
		} else if (result > 0) {
			return 1;
		} else {
			return 0;
		}
	}

	@Override
	public String toString() {
		return "DelayMessage [key=" + key + ", value=" + value + ", delayDate=" + delayDate + "]";
	}

}

DelayMessage类同时重写了getDelay(TimeUnit unit)和compareTo(T o),重写前者是为了获取当前对象的延迟过期时间,重写后者是为了便于根据元素的延迟过期时间排序,使队列头部的元素总是延迟时间最先到达的。当然,后者排序时调用了前者。
定义好消息的数据结构,我们再通过代码使用DelayQueue,代码如下:

/**
 * 测试DelayQueue
 * @author zhaoheng
 * @date 2019年5月26日
 */
public class DelayQueueTest {

	private static final Logger LOG = LoggerFactory.getLogger(DelayQueueTest.class);

	/**
	 * 延迟队列
	 */
	private static BlockingQueue<DelayMessage<String, String>> delayQueue = 
			new DelayQueue<DelayMessage<String, String>>();

	public static void main(String[] args) {

		final int capacity = 5; //队列容量
		
		// 生产者线程
		new Thread(() -> {

			for (int i = 1; i <= capacity; i++) {
				DelayMessage<String, String> msg = new DelayMessage<>("key" + i, String.valueOf(i),
						new Date(new Date().getTime() + i * 3000));
				try {
					// 生产者生产
					delayQueue.put(msg);
					LOG.info("生产者生产数据:{}", msg);
				} catch (InterruptedException e) {
					LOG.error("生产数据异常:", e);
				}
			}
		}).start();

		while (true) {
			// 等队列已满跳出循环
			if (delayQueue.size() == capacity) {
				break;
			}
		}

		LOG.info("生产数据完毕!!!");

		// 消费者线程
		new Thread(() -> {

			// 消费者消费
			try {
				while (true) {
					DelayMessage<String, String> msg = delayQueue.take();
					LOG.info("消费者消费数据:{}", msg);
					// 等队列为空跳出循环
					if (delayQueue.size() == 0) {
						break;
					}
				}
			} catch (InterruptedException e) {
				LOG.error("消费数据异常:", e);
			}

		}).start();

		while (true) {
			// 等队列为空跳出循环
			if (delayQueue.size() == 0) {
				break;
			}
		}
		LOG.info("消费数据完毕!!!");
	}
}

在DelayQueueTest类中,定义了一个全局的延迟队列,一个生产者线程,一个消费者线程。为了便于测试,要求消费者线程在生产者线程生产数据完毕后才可以消费队列中的元素。代码运行结果如下:

2019-05-26 12:04:25.348 - 生产者生产数据:DelayMessage [key=key1, value=1, delayDate=Sun May 26 12:04:28 CST 2019]
2019-05-26 12:04:25.351 - 生产者生产数据:DelayMessage [key=key2, value=2, delayDate=Sun May 26 12:04:31 CST 2019]
2019-05-26 12:04:25.351 - 生产者生产数据:DelayMessage [key=key3, value=3, delayDate=Sun May 26 12:04:34 CST 2019]
2019-05-26 12:04:25.351 - 生产者生产数据:DelayMessage [key=key4, value=4, delayDate=Sun May 26 12:04:37 CST 2019]
2019-05-26 12:04:25.351 - 生产者生产数据:DelayMessage [key=key5, value=5, delayDate=Sun May 26 12:04:40 CST 2019]
2019-05-26 12:04:25.351 - 生产数据完毕!!!
2019-05-26 12:04:28.349 - 消费者消费数据:DelayMessage [key=key1, value=1, delayDate=Sun May 26 12:04:28 CST 2019]
2019-05-26 12:04:31.352 - 消费者消费数据:DelayMessage [key=key2, value=2, delayDate=Sun May 26 12:04:31 CST 2019]
2019-05-26 12:04:34.352 - 消费者消费数据:DelayMessage [key=key3, value=3, delayDate=Sun May 26 12:04:34 CST 2019]
2019-05-26 12:04:37.352 - 消费者消费数据:DelayMessage [key=key4, value=4, delayDate=Sun May 26 12:04:37 CST 2019]
2019-05-26 12:04:40.352 - 消费者消费数据:DelayMessage [key=key5, value=5, delayDate=Sun May 26 12:04:40 CST 2019]
2019-05-26 12:04:40.352 - 消费数据完毕!!!

通过运行结果可以发现,延迟队列中的元素只有在延迟过期时,才可以被消费者消费。

那么,在工作过程中,DelayQueue可以被用在哪些场景呢?

DelayQueue的应用场景
  • 消息延迟消费或者任务延迟执行的场景
  • 缓存定时失效的场景
总结

至此,我们通过分析源码和程序测试的方式学习了DelayQueue。再次总结下其特点:

  • DelayQueue内部未实现队列元素的存储,存储借助于优先级队列PriorityQueue,而PriorityQueue内部是基于数组实现。所以,DelayQueue内部还是基于数组实现。
  • DelayQueue是一个无界队列,没有容量限制。
  • 队列内部的元素必须是Delayed类型,也就是元素必须实现Delayed接口。
  • 队列头部只有在延迟到期时才可以被移除,否则,移除操作会返回null。

由于笔主水平有限,笔误或者不当之处还请批评指正。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值