前言
- 在工作中经常会遇到需要延迟处理某些消息的业务场景,比如订单超时,延迟通知,任务延迟处理等等。
- 实现方式有多种,包括使用rabbitMQ死信队列处理,jdk的DelayQueue延迟队列,redission的延迟队列。
- 本次介绍jdk的DelayQueue延迟队列的使用及其实现原理。
过程
代码实现
- 使用DelayQueue延迟队列,需要将加入队列的对象实现Delayed接口,并重写其getDelay()方法和compareTo()方法;
- getDelay()方法是用于返回剩余过期时间,如果为小于等于0那就可以获取队列里的数据。
- compareTo()方法是用于判断在队列中的排序位置。
/**
* 延迟队列实现
*
* @AUTHOR ZRH
* @DATE 2021/4/24 0024
*/
public class Test2 {
public static void main(String[] args) {
DelayQueue<luoDelayed> delayQueue = new DelayQueue<>();
// 启动一个线程,循环获取队列里面的数据
new Thread(() -> {
while (true) {
try {
// 阻塞式获取队列数据,如果没有就一直阻塞着
luoDelayed take = delayQueue.take();
System.out.println("获取延迟队列数据:" + take.toString());
} catch (Exception e) {
System.err.println("异常。。。");
}
}
}).start();
Random random = new Random();
for (int i = 0; i < 10; i++) {
long time = System.currentTimeMillis();
luoDelayed luoDelayed = new luoDelayed();
luoDelayed.setValue(i);
luoDelayed.setEndTime(time + random.nextInt(2000) + 5000);
luoDelayed.setStartTime(time);
// 添加数据到队列里,过期时间至少是5秒后
delayQueue.offer(luoDelayed);
}
}
}
@Data
class luoDelayed implements Delayed {
private Integer value;
private Long endTime;
private Long startTime;
/**
* 返回剩余过期时间
*
* @param unit
* @return
*/
@Override
public long getDelay(TimeUnit unit) {
return unit.convert(endTime - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
}
/**
* 判断在队列中的排序位置
*
* @param o
* @return
*/
@Override
public int compareTo(Delayed o) {
return (int) (endTime - ((luoDelayed) o).getEndTime());
}
@Override
public String toString() {
return "luoDelayed(value=" + value + ",endTime=" + endTime + "),耗时=" + (System.currentTimeMillis() - startTime);
}
}
--------------------------------
执行结果:
获取延迟队列数据:luoDelayed(value=2,endTime=1619242103316),耗时=5010
获取延迟队列数据:luoDelayed(value=7,endTime=1619242103406),耗时=5101
获取延迟队列数据:luoDelayed(value=1,endTime=1619242103697),耗时=5392
获取延迟队列数据:luoDelayed(value=6,endTime=1619242103714),耗时=5409
获取延迟队列数据:luoDelayed(value=3,endTime=1619242104063),耗时=5758
获取延迟队列数据:luoDelayed(value=9,endTime=1619242104950),耗时=6645
获取延迟队列数据:luoDelayed(value=4,endTime=1619242104954),耗时=6649
获取延迟队列数据:luoDelayed(value=8,endTime=1619242104993),耗时=6687
获取延迟队列数据:luoDelayed(value=5,endTime=1619242105117),耗时=6812
获取延迟队列数据:luoDelayed(value=0,endTime=1619242105208),耗时=6904
简单探究下其延迟实现原理
/**
* Inserts the specified element into this delay queue.
*
* @param e the element to add
* @return {@code true}
* @throws NullPointerException if the specified element is null
*/
public boolean offer(E e) {
// 加锁操作
final ReentrantLock lock = this.lock;
lock.lock();
try {
// 往队列中添加一个数据,判断是否是队首数据
// 如果是,leader设置为null,并唤醒其他等待阻塞中的线程
q.offer(e);
if (q.peek() == e) {
leader = null;
available.signal();
}
return true;
} finally {
lock.unlock();
}
}
/**
* Retrieves and removes the head of this queue, waiting if necessary
* until an element with an expired delay is available on this queue.
*
* @return the head of this queue
* @throws InterruptedException {@inheritDoc}
*/
public E take() throws InterruptedException {
// 获取锁
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
//
for (;;) {
// 取出队列的队首数据
E first = q.peek();
if (first == null)
// 如果队列没有数据,就等待(阻塞),在加入数据到队列时被唤醒
available.await();
else {
// 如果队列不为空,就获取队列剩余过期时间,如果剩余过期时间小于等于0,就直接返回数据对象
long delay = first.getDelay(NANOSECONDS);
if (delay <= 0)
return q.poll();
// 释放first的引用,防止内存泄漏
first = null; // don't retain ref while waiting
// leader不为null,阻塞线程
if (leader != null)
available.await();
else {
// 把当前线程赋值给leader
Thread thisThread = Thread.currentThread();
leader = thisThread;
try {
// awaitNanos:等待经过delay纳秒后会自动唤醒,这时队首的数据就正好过期可以取出
available.awaitNanos(delay);
} finally {
// 释放leader的引用
if (leader == thisThread)
leader = null;
}
}
}
}
} finally {
// 解锁操作,并且如果leader为null且队列不为null的情况下,用signal唤醒其他等待的线程。
if (leader == null && q.peek() != null)
available.signal();
lock.unlock();
}
}
这里解释下first的内存泄漏情况
first = null; // don't retain ref while waiting
- 因为在多消费端调用take()方法时,如果线程A获取first = q.peek()并且进入后面的else流程把leader设置为自身线程,线程B也获取first = q.peek()然后阻塞了。
- 线程A把到期的对象poll出去,这时对象应该被GC回收掉。但是它还被线程B的first引用着,所以不能被GC掉。
- 如果还有其他的线程一直对first引用着,那么就会造成内存泄漏。
这里解释下leader的作用
/**
* Thread designated to wait for the element at the head of
* the queue. This variant of the Leader-Follower pattern
* (http://www.cs.wustl.edu/~schmidt/POSA/POSA2/) serves to
* minimize unnecessary timed waiting. When a thread becomes
* the leader, it waits only for the next delay to elapse, but
* other threads await indefinitely. The leader thread must
* signal some other thread before returning from take() or
* poll(...), unless some other thread becomes leader in the
* interim. Whenever the head of the queue is replaced with
* an element with an earlier expiration time, the leader
* field is invalidated by being reset to null, and some
* waiting thread, but not necessarily the current leader, is
* signalled. So waiting threads must be prepared to acquire
* and lose leadership while waiting.
*/
private Thread leader = null;
- 其在源码中的介绍是:用leader来减少不必要的等待时间。
- 当多个消费端调用take()方法,进入内部加锁,然后每个线程都peek队首数据。
- 如果leader不为null,说明已经有线程在处理当前对象,然后设置当前线程阻塞等待。
- 如果leader为null,说明没有线程在处理,然后当前线程就等待delay过期后,poll对象。
最后
- 如果有什么地方写的不对的,欢迎指出,我确认后会加以改正 -_-
- 虚心学习,共同进步 -_-