延时队列

356 阅读3分钟

这是我参与 8 月更文挑战的第 4 天,活动详情查看: 8月更文挑战

延时队列

可以通过延时队列,执行定时任务.

将定时任务放入队列中,队列会自动排序,当时间为0(到达定时时间),会将该任务放到队列头部,那么我们就可以取出定时任务,然后进行操作.

DelayQueue是Delayed元素的一个无界阻塞队列,只有延迟期满才能从队列中提取元素,队列的头部就是延迟期满后保存时间最长的Delayed元素.如果延迟期没有满,队列头部就没有元素,poll返回null.

当元素的getDelay 方法返回一个小于等于0的值,就会发生到期.

实现Delayed接口需要实现两个方法

image.png

getDelay()这个方法是Delayed的方法

这个方法返回的就是延迟期,当延迟期满元素就会放到队列头部,可以对元素进行取出等操作.

compareTo() 这个方法是Comparable的方法

通过这个方法,将队列中的元素进行排序,通常将getDelay作为排序标准

方法

  • take(): 阻塞方法,直到获取延迟期满的元素并移除

  • put():将指定元素插入队列

    • 和offer一样
  • offer(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();
            }
        }
    
  • poll():获取并移除头部延迟期满的元素,如果没有延迟期满的元素,就返回null

  • peel():获取但不移除队列头部,如果队列为空,返回null

  • size(): 返回元素数

  • clear(): 自动移除队列中所有元素

实现

1.填充的元素需要实现Delayed接口,因此可以使实体类实现这个接口,也可以将实体类封装一层再实现,不会破坏实体类的功能性.

public class TaskDelayed implements Delayed {

    private ServicePackTask packTask;

    // 获得延迟时间,用 发送时间-当前时间
    @Override
    public long getDelay(TimeUnit unit) {
//        return unit.convert((packTask.getPushTime().getTime() - System.currentTimeMillis()) / 1000 , TimeUnit.SECONDS);
        return (this.packTask.getPushTime().getTime() - System.currentTimeMillis()) /1000;
    }

    // 进行延时队列内部的比较,延迟时间短的放在头部
    @Override
    public int compareTo(Delayed o) {
        return (int) (this.getDelay(TimeUnit.SECONDS) - o.getDelay(TimeUnit.SECONDS));
    }
}

2.实现生产者和消费者方法

public void provider(ServicePackTask servicePackTask) {
        TaskDelayed taskDelayed = new TaskDelayed(servicePackTask);
        queue.offer(taskDelayed);
    }

生产者比较简单,只要调用方法,就会将元素放入队列中

消费者的话其实就是无限循环读取.首先先判断队列中是否有数据,如果没有就睡眠一段时间再循环(减少CPU压力).

如果队列不为空,就使用take方法(会自动阻塞线程直到有元素的延迟期满)取出元素并对元素进行操作.

public void consumer() {
        while (true) {
            if (0 == queue.size()) {
                System.out.println("完结");
                break;
            }
            TaskDelayed take = null;
            try {
                take = queue.take();
                System.out.println(take.getPackTask().getPushTime());
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
    }

3.封装方法

使用两个线程将生产和消费隔开.

为什么要将两个方法放到一起?

因为消费是队列自动计算的,但是如果我们不去触发,就无法对队列中的元素进行操作,所以要将消费放入一个可以出发的地方,就是生产的地方.

但是需要使用多线程将两个方法隔开,否则消费者将无法阻塞生产的方法.

public void start(ServicePackTask servicePackTask) {
        new Thread(new Runnable() {
            @Override
            public void run() {
                provider(servicePackTask);
            }
        }).start();

        new Thread(new Runnable() {
            @Override
            public void run() {
                consumer();
            }
        }).start();
    }