java并发队列之延时队列DelayQueue(五)

java并发队列之延时队列DelayQueue(五)

DelayQueue这是一个无界的延时阻塞队列. DelayQueue内部是使用优先级队列PriorityQueue实现的,使用时间来做优先级的延时阻塞队列
DelayQueue = BlockingQueue + PriorityQueue + Delayed

实战


import lombok.extern.slf4j.Slf4j;

import java.util.concurrent.BlockingQueue;
import java.util.concurrent.DelayQueue;
import java.util.concurrent.Delayed;
import java.util.concurrent.TimeUnit;

@Slf4j
public class DelayQueueDemo {


    public static void main(String[] args) {

        final BlockingQueue<DelayedElement> deque = new DelayQueue<>();
        Runnable producerRunnable = new Runnable() {
            int i = 0;

            public void run() {
                while (true) {
                    try {
                        ++i;
                        log.info("我生产了一个===" + i);
                        deque.put(new DelayedElement(1000 * i, "i=" + i));
                        Thread.sleep(200);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };

        Runnable customerRunnable = new Runnable() {
            public void run() {
                while (true) {
                    try {
                        log.info("我消费了一个===" + deque.take().msg);
                        //Thread.sleep(500);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        };

        Runnable getSize= new Runnable() {
            @Override
            public void run() {
                while (true) {
                    log.info("size={}",deque.size());
                    try {
                        Thread.sleep(1000);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }

                }
            }
        };

        Thread thread1 = new Thread(producerRunnable);
        thread1.start();

        Thread thread2 = new Thread(customerRunnable);
        thread2.start();

        Thread thread3 = new Thread(getSize);
        thread3.start();

    }

    static class DelayedElement implements Delayed {
 
        private final long expire;  //到期时间
        private final String msg;   //数据 

        public DelayedElement(long delay, String msg) { 
            this.msg = msg;
            expire = System.currentTimeMillis() + delay;    //到期时间 = 当前时间+延迟时间 
        }

        @Override
        public long getDelay(TimeUnit unit) {
            return unit.convert(this.expire - System.currentTimeMillis(), TimeUnit.MILLISECONDS);
        }

        //延迟队列是一个根据时间来排序的一个优先级队列
        @Override
        public int compareTo(Delayed o) {
            return (int) (this.getDelay(TimeUnit.MILLISECONDS) - o.getDelay(TimeUnit.MILLISECONDS));
        }
    }
}
输出结果:
[Thread-0] 2018-12-13 17:29:54.883 - 我生产了一个===1
[Thread-2] 2018-12-13 17:29:54.883 - size=0
[Thread-0] 2018-12-13 17:29:55.091 - 我生产了一个===2
[Thread-0] 2018-12-13 17:29:55.295 - 我生产了一个===3
[Thread-0] 2018-12-13 17:29:55.498 - 我生产了一个===4
[Thread-0] 2018-12-13 17:29:55.699 - 我生产了一个===5
[Thread-2] 2018-12-13 17:29:55.888 - size=5
[Thread-1] 2018-12-13 17:29:55.888 - 我消费了一个===i=1
[Thread-0] 2018-12-13 17:29:55.900 - 我生产了一个===6
[Thread-0] 2018-12-13 17:29:56.103 - 我生产了一个===7
[Thread-0] 2018-12-13 17:29:56.304 - 我生产了一个===8
[Thread-0] 2018-12-13 17:29:56.505 - 我生产了一个===9
[Thread-0] 2018-12-13 17:29:56.709 - 我生产了一个===10
[Thread-2] 2018-12-13 17:29:56.890 - size=9
[Thread-0] 2018-12-13 17:29:56.912 - 我生产了一个===11
[Thread-1] 2018-12-13 17:29:57.096 - 我消费了一个===i=2
[Thread-0] 2018-12-13 17:29:57.116 - 我生产了一个===12
[Thread-0] 2018-12-13 17:29:57.320 - 我生产了一个===13
[Thread-0] 2018-12-13 17:29:57.522 - 我生产了一个===14
[Thread-0] 2018-12-13 17:29:57.723 - 我生产了一个===15
[Thread-2] 2018-12-13 17:29:57.895 - size=13
[Thread-0] 2018-12-13 17:29:57.924 - 我生产了一个===16
[Thread-0] 2018-12-13 17:29:58.127 - 我生产了一个===17

结果分析:生产者生产了n多延时任务 ,第一个生产者延时1s,消费者只能在1s后才能获取到,在获取不到时阻塞当前线程.第二个生产者的内容需要延时2s,消费者只能在2s后才能获取到值.
注意:延时时间是2s,这个2s是从put压入成功后开始计算的.而不是消费者获取时才开始计算的,生产者压入一个2s的延时值后,过了2s后,消费者可以随时获取到值.

特点

  • 压入队列的元素必须实现Delayed接口.接口有两个方法:一个getDelay获取延时的时间,延时多久自己说了算,自己定义;第二个方法是compareTo,由于延时队列是使用PriorityQueue优先级队列来实现的,延时队列出队列的时候应该是延时时间最短的那个,所以这里需要自己来根据时间编写排序规则.
  • getDelay设置延时的时间,而compareTo则设置到期后的元素的出队列顺序.如果compareTo设置的错误或者不写,则会导致出队列时顺序错乱,起不到延时优先级的效果.比如设置过期时间为:1s,2s,3s,4s,5s这五个元素,元素一1s后到期应该放在队列头,第一个出队列.但是如果compareTo算法设置了是按时间倒序的.那么第一个出队列的是5s这个元素.但是5s这个元素需要等待5s之后才能获取到.这个时候本来是等待1s就应该获取到元素的,结果现在需要等待5s才能获取到元素.
  • 无界的一个队列.由于队列内部使用的是PriorityQueue队列实现的,所以put方法也没有阻塞.只有在获取take时在阻塞队列.所以也属于半阻塞队列.
  • 用处非常广泛.比如缓存.
  • 使用独占锁来保证队列的安全性.
  • 由于使用PriorityQueue队列做为内部存储结构,所以扩容,队列大小全部由PriorityQueue内部完成.
  • size中包含了到期的和未到期的.(移除的就不算了哦,哈哈)

源码分析

非阻塞的put,offer,add

public void put(E e) {
        offer(e);
    }
public boolean add(E e) {
        return offer(e);
    }
public boolean offer(E e) {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            //压入队列
            q.offer(e);
            //由于q 是优先级队列,所以添加元素后,调用q.peek(方法返回的并不一定是当前添加的元素。如果判断结果为true ,则说明当前元素e是最先将过期的,那么重置leader线程为null,这时候激活avaliabe变量条件队列里面的一个线程,告诉它队列里面有元素了。
            if (q.peek() == e) {
                leader = null;
                available.signal();
            }
            return true;
        } finally {
            lock.unlock();
        }
    }    

阻塞方法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(TimeUnit.NANOSECONDS);
                    //如果<=0,表示已经到期了或者过期了.
                    if (delay <= 0)
                        return q.poll();//弹出队列
                    //如果leader不等于空表示当前线程前面已经有一个线程执行到下面了(aaaaa),进入时间等待了.
                    //那么此时直接等待挂起.
                    else if (leader != null)
                        available.await();
                    else {
                    	//获取当前线程
                        Thread thisThread = Thread.currentThread();
                        leader = thisThread;
                        try {
                        	 //设置线程的等待时间为延时队列元素的过期时间(aaaaa)
                        	 //这里和上面的leader!=null结合起来,表示:只有第一个线程获取到头部元素后进入时间阻塞队列.
                        	 //其他的线程则直接进入await中.
                            available.awaitNanos(delay);
                        } finally {
                        	  //available.awaitNanos这里时间到时会自动唤醒线程.如果为true表示唤醒的线程就是当前线程,则把leader设置为null,其他线程进来之后就可以跳过leader!=null这里了.也可以进入带时间的等待中,而不是直接进入等待中.
                            if (leader == thisThread)
                                leader = null;
                        }
                    }
                }
            }
        } finally {
            if (leader == null && q.peek() != null)
                available.signal();
            lock.unlock();
        }
    }

实际利用可参考

编写一个缓存
简单demo

你可能感兴趣的:(Java多线程全面解刨)