DelayQueue延迟处理任务记录一下

前言

  • 在工作中经常会遇到需要延迟处理某些消息的业务场景,比如订单超时,延迟通知,任务延迟处理等等。
  • 实现方式有多种,包括使用rabbitMQ死信队列处理,jdk的DelayQueue延迟队列,redission的延迟队列。
  • 本次介绍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对象。

最后

  • 如果有什么地方写的不对的,欢迎指出,我确认后会加以改正 -_-
  • 2
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值