阻塞队列之DelayQueue

DelayQueue

一、类结构

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-Z3upEzpL-1585578489822)(C:\Users\MI\AppData\Roaming\Typora\typora-user-images\1585324001471.png)]

  1. 从继承关系看,实现了BlockingQueue接口,因此是一个阻塞队列;

  2. 实现了Delayed接口,因此是一个延迟队列,其在指定时间才能获取队列元素,队列头元素是最接近过期的元素。没有过期元素的话,使用poll()方法会返回null值,超时判定是通过getDelay(TimeUnit.NANOSECONDS)方法的返回值小于等于0来判断。延时队列不能存放空元素。

  3. 该延时队列实现了Iterator接口,但遍历顺序不保证是元素的实际存放顺序。

Delayed接口
public interface Delayed extends Comparable<Delayed> {

    /**
     * 返回剩余到期时间
     */
    long getDelay(TimeUnit unit);
}

二、类属性

//使用可重入锁
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();

因为使用了优先级队列,因此延迟队列是无界的

三、构造函数

	 public DelayQueue() {}

//指定初始集合元素
    public DelayQueue(Collection<? extends E> c) {
        this.addAll(c);
    }

四、入队操作

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) {//堆顶元素是e时,即入队成功
                leader = null;//等待的线程置为null
                available.signal();//唤醒等待的队列
            }
            //返回true
            return true;
        } finally {
            //释放锁
            lock.unlock();
        }
    }

流程:

  1. 加锁
  2. 入队
  3. 如果优先级队列堆顶是添加的新元素,说明入队成功,置排队线程为空,并唤醒在available队列中等待的线程
  4. 释放锁

其他几个入队都是直接调用offer()方法

五、出队

5.1 poll
 public E poll() {
        final ReentrantLock lock = this.lock;
        lock.lock();//上锁
        try {
            E first = q.peek();//获取堆顶元素(未出队)
            if (first == null || //堆顶为空,即队列为空,直接返回null
                first.getDelay(NANOSECONDS) > 0)//没到期就返回null
                return null;
            else
                //到期了就返回第一个元素
                return q.poll();
        } finally {
            //释放锁
            lock.unlock();
        }
    }
  1. 上锁
  2. 查看堆顶元素,如果为空或还未到期限,则返回null
  3. 如果到期了则堆顶元素出队并返回
  4. 释放锁

因此poll方法是不阻塞的,堆顶元素为空或没到期限时直接返回null

5.2 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)//小于0,说明到时间了
                        return q.poll();//出队返回
                    first = null; //还没到期,则置为null
                    if (leader != null)//等待线程非空,说明有线程在等待
                        available.await();//阻塞等待
                    else {//没有线程等待
                        Thread thisThread = Thread.currentThread();
                        leader = thisThread;//将等待线程设为当前线程
                        try {
                            available.awaitNanos(delay);//等待delay时间
                        } finally {
                            //delay时间后等待线程还是当前线程,则清空,重新循环获取堆顶元素
                            if (leader == thisThread)
                                leader = null;
                        }
                    }
                }
            }
        } finally {
            //到这里说明结束了自旋,出队成功
            //如果没有线程在等待,且堆顶还有元素,就再唤醒一个等待线程
            if (leader == null && q.peek() != null)
                available.signal();
            //释放锁
            lock.unlock();
        }
    }

流程:

  1. 加锁
  2. 获取堆顶元素,如果为空,则直接阻塞等待
  3. 如果不为空,则再判断该元素到期了没,如果到期了,就将堆顶元素出队返回
  4. 如果没到期,则判断是否已经有线程在等待,如果有,则阻塞等待
  5. 如果没有线程在等待,则将等待线程设为当前线程,并阻塞等待delay时间自动唤醒,同时清空等待线程,进入下个循环再次获取
  6. 如果没有线程在等待且堆顶还有元素,就再唤醒一个等待线程
  7. 释放锁

take方法在队列为空或未到期时会进行阻塞等待

还有一个带超时的poll(long timeout, TimeUnit unit),跟take逻辑基本一样,只是在堆顶为空时等待timeout时间,nanos小于延迟时间时等待nanos时间

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();
                if (first == null) {
                    if (nanos <= 0)
                        return null;
                    else
                        nanos = available.awaitNanos(nanos);
                } else {
                    long delay = first.getDelay(NANOSECONDS);
                    if (delay <= 0)
                        return q.poll();
                    if (nanos <= 0)
                        return null;
                    first = null; // don't retain ref while waiting
                    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();
        }
    }

六、示例

package cn.xys.syn;

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

/**
 * @author MaoLin Wang
 * @date 2020/3/2816:46
 */
public class DelayedQueueTest {

    public static void main(String[] args) {
        DelayQueue<Message> queue = new DelayQueue<>();
        long start = System.currentTimeMillis();
        new Thread(() -> {
            while (true) {
                try {

                    System.out.println(queue.take().deadLine - start);

                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }).start();
        queue.add(new Message(start+2000));
        queue.add(new Message(start+12000));
        queue.add(new Message(start+9000));
        queue.add(new Message(start+6000));
        queue.add(new Message(start+33000));
    }
}

class Message implements Delayed {

    long deadLine;

    public Message(long deadLine) {
        this.deadLine = deadLine;
    }

    @Override
    public long getDelay(TimeUnit unit) {
        return deadLine - System.currentTimeMillis();
    }

    @Override
    public int compareTo(Delayed o) {
        return (int) (getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS));

    }
}

结果:

2000
6000
9000
12000
33000

七、总结

  1. 内部使用优先级队列,实现Delayed接口,将延迟时间作为优先级,早到期的早出队,因此可以用于定时任务
  2. 是一个阻塞队列,取一个元素后如果队列非空,还会唤醒下一个线程
  3. 内部使用重入锁加一个条件队列实现并发安全,条件队列用于队列为空时,部分方法如带超时的poll和take会进行阻塞等待
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值