• 关于DelayQueue


     在团队的重试与延迟调度项目中,使用了基于DelayQueue的方式来实现时间调度和触发,此处,对DelayQueue进行一些梳理。

    首先是Queue接口,关于队列的含义,不再赘述。如下,Queue接口方法,按抛异常或返回特定值(null)可以分为两类如下图:  

    接下来是BlockingQueue接口,其继承了Queue接口,方法如下:

    注意:BlockingQueue不接受null值,其所有实现类对于add null操作,都会抛出NullPointerException。null只作为poll、peek等操作的失败值。在Java官方文档中,即说明了BlockingQueue及其实现类主要用来适用生产者-消费者模型,将其作为普通的集合使用性能可能会存在问题。同时,其所有实现类都是线程安全的,内部方法通过各种锁变成了原子性操作。以下是官方文档给出的基于BlockingQueue的生产者-消费者示例:

    class Producer implements Runnable {
       private final BlockingQueue queue;
       Producer(BlockingQueue q) { queue = q; }
       public void run() {
         try {
           while (true) { queue.put(produce()); }
         } catch (InterruptedException ex) { ... handle ...}
       }
       Object produce() { ... }
     }
    
     class Consumer implements Runnable {
       private final BlockingQueue queue;
       Consumer(BlockingQueue q) { queue = q; }
       public void run() {
         try {
           while (true) { consume(queue.take()); }
         } catch (InterruptedException ex) { ... handle ...}
       }
       void consume(Object x) { ... }
     }
    
     class Setup {
       void main() {
         BlockingQueue q = new SomeQueueImplementation();
         Producer p = new Producer(q);
         Consumer c1 = new Consumer(q);
         Consumer c2 = new Consumer(q);
         new Thread(p).start();
         new Thread(c1).start();
         new Thread(c2).start();
       }
     }
    

     接下来,开始聊聊DelayQueue

    先说说其指定的队列元素:interface Delayed的实现类
    Delayed有两个接口方法:

    long getDelay(TimeUnit unit);    //返回还剩余的延迟时间
    public int compareTo(T o);       //Delayed接口继承了Comparable,用来排序
    

    接下来,就是delayQueue的具体实现了,首先值得注意的是其内部持有这么几个东西:

    ReentrantLock lock = new ReentrantLock();              // 可重入锁
    Condition available = ReentrantLock.newCondition();   // 靠其来发信号唤醒等着拿数据的线程
    private Thread leader = null;                          // leader-follower模式,用来最小化等待队头元素时间的策略
    PriorityQueue<E> q = new PriorityQueue<E>();           // 优先队列,可以知道Delayed.compareTo()必然用在此处
    

    然后看看其两个核心方法,put与take:

        public void put(E e) {
            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();
            }
        }
    

    可以看出,在向delayQueue添加元素时,主要调用PriorityQueue.offer()向内部的优先队列添加元素。在优先队列中添加元素时,会按照事先的比较方法,找到元素的合适位置并插入。同时,看到PriorityQueue中是使用Obeject[]来存储数据的,因此可以推测在大量的向delayQueue添加随机而无序的元素时,可能会遇到性能问题。
    再一个有点意思的就是在offer到PriorityQueue之后,会检查一下自己是不是队头,要是自己是队头的话,作废老的leader(因为老的leader是目标取走前一个队头的线程),再让接下来最先来取走队头的线程成为leader(结合take方法看,下一次最先来的线程,才是取走当前队头的线程)。

    public E take() throws InterruptedException {
            final ReentrantLock lock = this.lock;
            lock.lockInterruptibly();
            try {
                for (;;) {
                    E first = q.peek();
                    if (first == null)
                        available.await();          // 1
                    else {
                        long delay = first.getDelay(TimeUnit.NANOSECONDS);
                        if (delay <= 0)             // 2
                            return q.poll();
                        else if (leader != null)    // 3
                            available.await();
                        else {                      // 4
                            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();
            }
        }
    

    来分析下上面的代码,
    1. 当first == null,说明队列为空,当前线程使自己沉睡,并交出ReentrantLock锁,等待Condition.signal()来唤醒自己
    2. 当队头元素到达指定的延迟时间了,直接取出返回
    3. 当leader != null,说明前面的leader还在等呢,那么当前线程肯定是follower,那当前线程就只能无奈的等着Condition发信号了
    4. 当leader = null时,说明当前线程是第一个要取走队头的线程,那它就理所当然成为leader,后边再来拿队头的,都是follower,都得排在后边等着。而这时,当前线程又清楚的知道队头元素的延迟时间啥时候到期,那么它根本就不用等Condition发信号,自己睡到延迟时间到期,再醒来肯定就能直接拿走队头了。可以看出,通过引入leader-follower能够降低线程的等待时间。

    5. 再一个稍微注意点的就是无限for循环了,当被condition.singal()唤醒之后,正好进行下一个循环,接着取

  • 相关阅读:
    camke中关于变量的一些知识
    ububtu16.04下安装protobuf
    ububtu下安装配置搜狗输入法
    anaconda资源链接
    把本地项目文件上传到github远程仓库的教程
    cmake practice一文中安装可执行文件的方法
    slam学习资源
    js中对String去空格
    去当前时间的周一
    对sql初始化的值的处理
  • 原文地址:https://www.cnblogs.com/dosmile/p/6593031.html
Copyright © 2020-2023  润新知