Java Interview in Action - DelayQueue

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

使用 PriorityQueue 实现

队列中的元素必须实现 Delay 接口

class Message implements Delayed{

    @Override
    public long getDelay(TimeUnit unit) {
        return 0;
    }

    @Override
    public int compareTo(Delayed o) {
        return 0;
    }
}

重写其中的 getDelay(TimeUnit) 和 compareTo(Delayed) 方法

创建元素时,可以指定多久才能从队列中获取当前元素

只有在延迟期满的时才能从队列中提取元素

关注下添加元素的时候设置时间,以及获取的时候判断到期

应用场景:

① 缓存系统设计:保存缓存元素的有效期,一旦能从 DelayQueue 中获取元素,则表示缓存已失效

② 定时任务调度 :保存系统要执行的任务和任务执行时间是,一旦从 DelayQueue中获取到任务就开始执行,比如 Timer使用 DelayQueue 实现

似乎,黑马redis哪个利用消息队列的也用到了 DelayQueue, 又好像是 ArrayBlockingQueue,验证一下

1. DelayQueue 使用方式

public class DelayQueueDemo {
    public static void main(String[] args) throws InterruptedException {
        Message item1 = new Message("message 1", 5, TimeUnit.SECONDS);
        Message item2 = new Message("message 2", 10, TimeUnit.SECONDS);
        Message item3 = new Message("message 3", 15, TimeUnit.SECONDS);
        
        DelayQueue<Message> queue = new DelayQueue<>();
        queue.put(item1);
        queue.put(item2);
        queue.put(item3);

        for(int i = 0; i < 3; i++){
            Message take = queue.take();
            System.out.println(take);
        }
    }
}

class Message implements Delayed{

    private long time;
    private String content;

    public Message(String content, long time, TimeUnit unit){
        this.content = content;
        this.time = System.currentTimeMillis() + (time > 0 ? unit.toMillis(time) : 0);
    }

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

    @Override
    public int compareTo(Delayed o) {
        Message item = (Message) o;
        long diff = this.time - item.time;
        return diff <= 0 ? -1 : 1;
    }

    @Override
    public String toString() {
        return "Message { " + "time = " + time + ", content = " + content + '}';
    }
}

运行结果:

Message { time = 1653636422281, content = message 1}
Message { time = 1653636427281, content = message 2}
Message { time = 1653636432281, content = message 3}

示例说明:

① Message 类实现 Delayed 类,实现其中的 getDelay(TimeUnit) 方法

由于 Delayed 接口继承了 Comparable<Delayed>,因此还需要重写 ComparaTo(Delayed)方法

② main 方法中创建 DelayQueue<Message>对象,创建 message(String, long, TimeUnit)对象

③ 调用 delayQueue.put(e),delayQueue.take() 添加元素和获取元素

public interface Delayed extends Comparable<Delayed> {
	// 返回值 <= 0 表示已过期
    long getDelay(TimeUnit unit);
}

2. DelayQueue 属性

private final transient ReentrantLock lock = new ReentrantLock();
private final PriorityQueue<E> q = new PriorityQueue<E>();
// 指定用于等待队列开头元素的线程,Leader-Follower模式变体,用于最小化不必要的定时等待
private Thread leader = null;
// 当更新的元素在队列头部变得可用时,或新线程可能需要成为领导者时,会发出条件信号
private final Condition available = lock.newCondition();

3. DelayQueue 构造方法

① 无参构造

public DelayQueue() {}

② 有参构造,以 Delayed 实例集合作为入参

public DelayQueue(Collection<? extends E> c) {
        this.addAll(c);
    }

调用 AbstractQueue 中的 addAll(Collection<? extends E>)方法

public boolean addAll(Collection<? extends E> c) {
    if (c == null)
        throw new NullPointerException();
    if (c == this)
        throw new IllegalArgumentException();
    boolean modified = false;
    for (E e : c)
        if (add(e))
            modified = true;
    return modified;
}

调用DelayQueue 中重写的 add(e) 方法

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

此处注意下 DelayQueue 与 AbstractQueue 之间的关系

public class DelayQueue<E extends Delayed> extends AbstractQueue<E>
    implements BlockingQueue<E>

DelayQueue 继承了 AbstractQueue,但是 DelayQueue 中没有 addAll(Collection<? extends E>) 方法,有 add(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();
    }
}

什么情况下返回false 呢? 除非抛出异常,否则不会返回 false,因为未设置 return false

在这里插入图片描述

除非抛出异常,否则应该都会返回 true

上述过程:只有 available.signal 部分可能抛出异常,不会返回 true

4. 入队方法

4. 1 add(E e)

问题,add(), put() 都调用了 offer() 方法,注释部分为什么说会抛出 NullPointerException异常呢?

add(E e)

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

4.2 put(E e)

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

add(E e) 和 put(E e) 的区别为是否有返回值

add 有返回值,put 没有

add,put 都不允许添加 null 值,报异常是调用 priorityQueue.offer(E e)

public boolean offer(E e) {
    if (e == null)
        throw new NullPointerException();
    ...
    return true;
}

delayQueue.put(null) 之后

Exception in thread "main" java.lang.NullPointerException
	at java.util.PriorityQueue.offer(PriorityQueue.java:335)

发现是 PriorityQueue 中的异常

因 DelayQueue 数据结构是用 PriorityQueue 实现的

4.3 offer(E, long, TimeUnit)

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

问题:这里的 timeout 参数没有传递,那 offer(E, long, TimeUnit) 的意义何在?

4.4 offer(E e)

public boolean offer(E e) {
	// 1. 获取可重入锁
    final ReentrantLock lock = this.lock;
    // 2. 加锁
    lock.lock();
    try {
    	// 3. 调用 priorityQueue.offer(e)
        q.offer(e);
        // 4. 优先队列的堆顶元素为添加的元素
        if (q.peek() == e) {
            leader = null;
            available.signal();
        }
        // 5. 返回添加成功
        return true;
    } finally {
    	// 6. 释放锁
        lock.unlock();
    }
}

问题: q.ppek() == e,为什么会执行 后面的 signal()

延时体现在哪里?

leader 的作用是什么?

4.4.1 priorityQueue.offer(e)

PriorityQueue 中的 offer() 方法

public boolean offer(E e) {
	// 1. 若插入元素为null,抛出 NullPointerException 异常
    if (e == null)
        throw new NullPointerException();
    // 2. 修改优先级队列结构性变化次数
    modCount++;
    // 3. 获取当前优先级队列的元素个数
    int i = size;
    // 4. 若当前元素个数 >= 优先级队列的容量则扩容
    if (i >= queue.length)
        grow(i + 1);
    // 5. 队列元素个数 size++
    size = i + 1;
    // 6.1 若队列为空,则将元素放到数组的第0个位置
    if (i == 0)
        queue[0] = e;
    else
    // 6.2 队列不为空,则插入元素并调整小顶堆使之平衡
        siftUp(i, e);
    // 7. 返回插入成功
    return true;
}

在这里插入图片描述
PriorityQueue 使用小顶堆实现

问题:PriorityQueue 中堆和数组的关系

堆就是数组实现的,只是下标间的关系用小顶堆的逻辑处理??

5. 出队方法

5.1 poll()

poll() 方法用于检索和移除队列的首个元素,若队列中没有延时过期的元素则返回 null

getDelay(TimeUnit) 方法由添加的元素对应的实体类重写 Delayed 接口中的getDelay(TimeUnit) 实现

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

在这里插入图片描述

返回值:

null:队列为空,或者队列不为空时,first元素的延时时间未过期

e :first != null 且 first.getDelay(TimeUnit) < 0

5.1.1 priorityQueue.peek()

public E peek() {
    return (size == 0) ? null : (E) queue[0];
}

若队列为空则返回null,否则返回 queue数组中的第一个元素

5.1.2 delayQueue.getDelay(NANOSECONDS)

参数 TimeUnit = NANOSECONDS

NANOSECONDS {
    public long toNanos(long d)   { return d; }
    public long toMicros(long d)  { return d/(C1/C0); }
    public long toMillis(long d)  { return d/(C2/C0); }
    public long toSeconds(long d) { return d/(C3/C0); }
    public long toMinutes(long d) { return d/(C4/C0); }
    public long toHours(long d)   { return d/(C5/C0); }
    public long toDays(long d)    { return d/(C6/C0); }
    public long convert(long d, TimeUnit u) { return u.toNanos(d); }
    int excessNanos(long d, long m) { return (int)(d - (m*C2)); }
}

使用方法:

public Message(String content, long time, TimeUnit unit){
    this.content = content;
    this.time = System.currentTimeMillis() + (time > 0 ? unit.toMillis(time) : 0);
}
@Override
public long getDelay(TimeUnit unit) {
    return time - System.currentTimeMillis();
}

利用 NANOSECONDS 中的 toMills(time) 将给定时间转化为 ms 为单位,对象加载的时候设定 time 为当前系统 ms时间 + 设定的转化后的ms值

在 getDelay(TimeUnit) 中比较 time 是否小于当前时间,小于则说明延时过期

测试:

① 将上述 take() 换成 poll()

Message item1 = new Message("message 1", 5, TimeUnit.SECONDS);

unit.toMills(time) = TimeUnit.SECONS.toMills(time) = 5 * 1000

因此若不加 延时,得到的结果均为 null,即当前未有元素过期

② 设置延时 60s,并调整元素添加顺序【将延迟时间短的最后添加】

Message item1 = new Message("message 1", 5, TimeUnit.SECONDS);
Message item2 = new Message("message 2", 10, TimeUnit.SECONDS);
Message item3 = new Message("message 3", 15, TimeUnit.SECONDS);
queue.put(item2);
queue.put(item3);
queue.put(item1);

输出结果:

Message { time = 1653654757632, content = message 1}
null
null

由于重写了 compareTo 方法,添加元素的时候,会根据比较器调整小顶堆,使得堆顶为最大值或最小值

若将 升序排列调整为降序排列 this.time - o.time修改为o.time - this.time

public int compareTo(Delayed o) {
    Message item = (Message) o;
    long diff = this.time - item.time;
    return diff <= 0 ? 1 : -1;
}

则输出结果为:

null
null
null

即PriorityQueue 调整小顶堆的时候,是根据compareTo 的返回结果,从这个角度来说,PriorityQueue 也可以实现大顶堆

③ 使用 take 检验输出顺序 ,修改 compareTo 中排序规则,改为降序

输出结果:

Message { time = 1653655662513, content = message 3}
Message { time = 1653655657513, content = message 2}
Message { time = 1653655652513, content = message 1}

getDelay :实现 Delayed 接口的实现类的实例化对象,作为添加到 DelayQueue 中的元素

5.1.3 priorityQueue.poll()

public E poll() {
	// 1. 若队列为空,则返回 null
    if (size == 0)
        return null;
    // 2. 获取删除元素后队列中剩余元素的数目,并更新 size
    int s = --size;
    // 3. 修该优先级队列结构性变化[添加,删除操作]次数
    modCount++;
    // 4. 获取队列中的首个元素即堆顶元素,并作为返回值 result
    E result = (E) queue[0];
    // 5. 获取队列中的最后一个元素的值
    E x = (E) queue[s];
    // 6. 释放数组 s 位置[第 s + 1 个元素]的空间
    queue[s] = null;
    // 7. 若优先级队列非空
    if (s != 0)
    	// 7.1 向下调整优先级队列
        siftDown(0, x);
    // 8. 返回原优先级队列头部元素
    return result;
}

5.2 poll(long, TimeUnit)

阻塞式获取,若超时或元素未过期则返回 null

public E poll(long timeout, TimeUnit unit) throws InterruptedException {
	// 1. 将设定的时间 timeout 转化为 ns 级
    long nanos = unit.toNanos(timeout);
    final ReentrantLock lock = this.lock;
    // 2. 设置可中断加锁
    lock.lockInterruptibly();
    try {
    	// 3. 循环阻塞式获取队列中的元素
        for (;;) {
        	// 4. 获取优先级队列中的堆顶元素 first
            E first = q.peek();
            // 4.1 若 first == null 即队列为空
            if (first == null) {
            	// 4.1.1 等待时间是否超时
                if (nanos <= 0)
                	// 等待超时返回 null
                    return null;
                else
                // 4.1.2 等待未超时,则先释放锁添加到条件队列等待唤醒,唤醒后再到同步队列中排队获取锁,再执行 1 -> 4
                // 线程获得锁后返回剩余等待时间
                    nanos = available.awaitNanos(nanos);
            } else {
            // 4.2 first != null 队列非空
            	// 4.2.1 通过重写的 comparator 方法判断延时是否过期
                long delay = first.getDelay(NANOSECONDS);
                // 4.2.2 延时过期,从优先队列中获取元素
                if (delay <= 0)
                    return q.poll();
                // 4.2.3 延迟未过期,判断等待时间是否超时,超时返回 null
                if (nanos <= 0)
                    return null;
                // 4.2.4 first 未过期且线程等待未超时, 设置 first = null ?
                first = null; // don't retain ref while waiting
                // 4.2.5 若剩余等待时间 nanos < delay 剩余到期时间, 或等待头结点的线程不为null则阻塞线程
                if (nanos < delay || leader != null)
                    nanos = available.awaitNanos(nanos);
                else {
                // 4.2.6 剩余等待时间 nanos > delay 剩余到期时间,且等待头节点的线程为 null
                	// 4.2.6.1 获取当前线程
                    Thread thisThread = Thread.currentThread();
					// 4.2.6.2 设置 leader 为当前线程
                    leader = thisThread;
                    try {
                    	// 4.2.6.3 阻塞线程 delay ns 即刚好元素延时到期时唤醒结点,返回剩余等待时间
                        long timeLeft = available.awaitNanos(delay);
                        // 4.2.6.4 返回线程等待直到元素到期时的剩余等待时间
                        nanos -= delay - timeLeft;
                    } finally {
                    	// 4.2.6.5 若 leader 仍未当前线程,说明当前线程醒来后获取到了元素,则设置 leader = null
                        if (leader == thisThread)
                            leader = null;
                    }
                }
            }
        }
    } finally {
        if (leader == null && q.peek() != null)
            available.signal();
        lock.unlock();
    }
}

leader :等待头部元素到期的线程

问题:什么情况下会发生 leader != thisThread 的情况?

问题:什么情况下会发生 leader != null 的情况?

注意上述 else 分支:剩余等待时间 > first 元素的剩余过期时间,线程阻塞唤醒之后,将 leader 设置为 null ,然后继续执行 for 循环中的处理逻辑,获取队列的首个元素

最后的finally 是在返回 priorityQueue.poll() 元素之前执行的

若非 leader != null 说明当前有等待队首元素的线程,则不会执行 唤醒其他线程的操作

若 leader == null 但 q.peek() == null即队列中没有元素,也不会执行 available.signal() 操作

poll 设置阻塞式获取锁

流程图画一下:
在这里插入图片描述

上述还有未搞明白的地方:

关于 leader 的设置:

为什么在内层 finally 中要判断 leader == thisThread

还有就是等待头结点的线程唤醒其他结点,默认不是非公平锁吗?

还有就是 若当前线程等待消息过期时 available.awaitNanos(delay) ,锁被其他线程获取呢?当前线程是否取不到等待的消息

在生产环节会造成什么问题

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

在这里插入图片描述
对比 poll(TimeUnit) 和 take 的流程图,基本处理逻辑相同,只是 poll(TimeUnit) 中多了等待超时的处理

以 take() 为例对获取元素的流程进行总结

① 通过q.peek()判断队列是否为空,为空则执行 await()

take–await()无限期等待,poll(nanos)–await(nanos)等待剩余时间

② 通过first.getDelay()获取元素的延时剩余时间,若延时过期则释放锁并执行q.poll()从队列中获取元素

③ 元素延时未过期, poll(nanos) 会判断等待是否超时,超时则返回 null

未超时,take(), poll(nanos) 处理流程相同,本轮循环未获取到元素则设置first=null回收

④ 通过leader != null判断是否要阻塞线程

poll(nanos) :执行前会先判断nanos < delay即当消息延时过期时间到时,等待时间是否超时

若超时则执行 awaitNanos(nanos),继续一轮for循环,直到 nanos <= 0,等待超时返回 null

若未超时和take() 接下来的处理流程相同

take():若有正在等待头结点的线程,则阻塞线程,线程被唤醒之后继续执行 for 循环

若无正在等待头结点的线程,则通过leader - thisThread设置当前线程为等待头节点的线程,

设置等待时间 await(delay)等待消息过期,线程等待超时后处理 leader【这里 if 判断还没捋清楚】

⑤ 线程等待消息过期后,再次执行for循环,获取到元素,执行 finally 解锁以及判断是否要唤醒线程后返回获取到的元素

6. 面试考点

6.1 延迟队列的特性

6.2 DelayQueue 的适用场景

6.3 DelayQueue 的工作原理

6.4 DelayQueue 对 ReentrantLock 和 Condition 的应用

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值