Java 并发包中并发队列原理剖析(二)ArrayBlockingQueue、PriorityBlockingQueue、DelayQueue

一、ArrayBlockingQueue

1、类图

在这里插入图片描述
    可以看到,ArrayBlockingQueue 内部有一个数组 items,用来存放队列元素,putIndex 变量表示入队元素下标,takeIndex 表示出队元素下标,count 统计队列元素个数。这些变量并没有使用 volatile 修饰,因为访问它们都是在锁块里,加锁已经保证了内存可见性。另外还有个独占锁 lock 用来保证入队、出队的原子性,还保证了 同时只有一个线程 可以入队、出队操作,另外,notEmpty、notFull 条件变量用来进行入队、出队的同步。
    ArrayBlockingQueue 是 有界队列 ,所以构造方法必须传入队列大小参数,源码:

public ArrayBlockingQueue(int capacity) {
		// (一)
        this(capacity, false);
    }

(一):

public ArrayBlockingQueue(int capacity, boolean fair) {
        if (capacity <= 0)
            throw new IllegalArgumentException();
        this.items = new Object[capacity];
        lock = new ReentrantLock(fair);
        notEmpty = lock.newCondition();
        notFull =  lock.newCondition();
    }

    

2、ArrayBlockingQueue 原理
(1) offer 操作

    向队列尾部插入一个元素,如果队列有空闲空间,则插入成功后返回 true,如果队列已满,则丢弃当前元素然后返回 false,如果 e 元素为 null 则抛出 NullPointerException 异常,该方法是不阻塞的。

源码:

   public boolean offer(E e) {
   		// e 为 null ,则抛出 NullPointerException 异常
        checkNotNull(e);

		// 获取独占锁
        final ReentrantLock lock = this.lock;
        
        lock.lock();
        try {
        	// 如果队列满则返回 false
            if (count == items.length)
                return false;
            else {
            	// (一)否则 插入元素
                enqueue(e);
                return true;
            }
        } finally {
            lock.unlock();
        }
    }

(一):

  private void enqueue(E x) {       
        final Object[] items = this.items;
        items[putIndex] = x;

		// 计算下标
        if (++putIndex == items.length)
            putIndex = 0;

		// 递增元素个数计数器
        count++;

		// 激活 notEmpty 条件队列中因为调用 take ,但队空 而被阻塞的线程
        notEmpty.signal();
    }

    

(2)put 操作

    向队列尾部插入一个元素,如果队列有空闲空间,则插入成功后返回 true,如果队列已满,则阻塞当前线程 直到队列有空闲并插入成功后返回 true。如果阻塞时被其他线程设置了中断标志,则 被阻塞的线程会抛出 InterruptedException 异常而返回。另外,如果 e 为 null 则抛出NullPointerException 异常。

源码:

public void put(E e) throws InterruptedException {
    checkNotNull(e);
    
    // 获取锁,可被中断
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
    
    	// 如果队列满,则把当前线程放入 notFull 管理的条件队列,while 循环避免虚假唤醒
        while (count == items.length)
            notFull.await();

		// 插入元素
        enqueue(e);
    } finally {
        lock.unlock();
    }
}

    

(3)poll 操作

    从队列头部获取并移除一个元素,如果队列为空,则返回 null,该方法是不阻塞的。
源码:

  public E poll() {
  		// 获取锁
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
        	// 当前队列为空则返回 null,    (一)否则调用 dequeue
            return (count == 0) ? null : dequeue();
        } finally {
            lock.unlock();
        }
    }

(一):

 private E dequeue() {
       
        final Object[] items = this.items;
        @SuppressWarnings("unchecked")
	
		// 获取元素值
        E x = (E) items[takeIndex];
        
		// 设置队头元素为 null
        items[takeIndex] = null;
        
        if (++takeIndex == items.length)
            takeIndex = 0;
            
        // 队列元素个数减 1    
        count--;
        if (itrs != null)
            itrs.elementDequeued();
            
		// 激活 notFull 条件队列中的一个线程
        notFull.signal();
        return x;
    }

    

(4)take 操作

    获取当前队列头部元素并从队列中移除它。如果队列为空,则阻塞当前线程,直到队列不为空,然后返回元素;如果在阻塞时 被其他线程设置了中断标志,则被阻塞线程会抛出 InterruptedException 异常而返回。

源码:

public E take() throws InterruptedException {
        final ReentrantLock lock = this.lock;
        lock.lockInterruptibly();
        try {
            while (count == 0)
            
            	// (一)
                notEmpty.await();
            return dequeue();
        } finally {
            lock.unlock();
        }
    }

    可以看到,take 操作的代码比较简单,比 poll 相比只是 (一)处不同,在这里,如果队列为空 ,则把当前线程挂起后放入 notEmpty 的条件队列,等其他线程调用 notEmpty.signal() 方法后在返回。
    

(5)peek 操作

    获取队列头部元素 但是不从队列中移除它,如果队列为空 则 返回 null,该方法是不阻塞的。

源码:

 public E peek() {
 		// 获取锁
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {   // (一)
            return itemAt(takeIndex); // null when queue is empty
        } finally {
            lock.unlock();
        }
    }

(一):

  /**
     * Returns item at index i.
     */
    @SuppressWarnings("unchecked")
    final E itemAt(int i) {
        return (E) items[i];
    }

    

(6)size 操作

    计算当前队列元素个数。
源码:

public int size() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            return count;
        } finally {
            lock.unlock();
        }
    }

    可以看到,size() 方法比较简单,获取锁后直接返回 count,并在返回前释放锁。❓为什么这里获取 count 值需要加锁呢 ❓ 因为获取锁的语义之一是,获取锁后的变量都要从主内存中获取,这样保证了内存的可见性
    
🎭总结:
     ArrayBlockingQueue 通过使用全局独占锁实现了同时只能有一个线程进行入队 或者 出队操作,这个锁的粒度比较大,有点类似在方法上添加 synchronized 的意思。其中 offer 和 poll 操作通过简单的加锁进行入队、出队操作,而 put、take 则使用条件变量,如果队列满则等待,如果队列空也等待,然后分别在出队和入队操作中发送信号激活等待线程 实现同步。另外,相比 LinkedBlockingQueue ,ArrayBlockingQueue 的 size 操作的结果是精确的,因为计算前加了全局锁。
    

二、PriorityBlockingQueue

1、类图

在这里插入图片描述

    可以看到,PriorityBlockingQueue 内部有一个数组 queue,用来存放队列元素,size 用来存放队列元素个数 ,allocationSpinLock 是个自旋锁,使用 CAS 操作来保证同时只有一个线程可以扩容队列,状态为 0 【当前没有进行扩容】或 1【当前真正扩容】。
    PriorityBlockingQueue是一个 带优先级的无界阻塞队列 ,每次出队都返回优先级最高 或者 最低的元素,所以有一个比较器 comparator 用来比较元素大小,默认使用对象的 compareTo 方法提供比较规则,如果用户想要自定义比较规则可以自定义 comparators 。
    notEmpty 条件变量用来实现 take 方法阻塞模式,没有 notFull 是因为这里的 put 是非阻塞的,这是无界队列。
    PriorityBlockingQueue 内部是使用平衡二叉树实现的,所以直接遍历队列元素不保证有序。

构造方法源码:

 public PriorityQueue() {
 	 // (一) (二)
        this(DEFAULT_INITIAL_CAPACITY, null);
    }

(一):

public PriorityQueue(int initialCapacity,
                         Comparator<? super E> comparator) {
        // Note: This restriction of at least one is not actually needed,
        // but continues for 1.5 compatibility
        if (initialCapacity < 1)
            throw new IllegalArgumentException();
        this.queue = new Object[initialCapacity];
        this.comparator = comparator;
    }

(二):

private static final int DEFAULT_INITIAL_CAPACITY = 11;

    可以看到,默认队列容量是 11,默认比较器是 null,也就是使用元素的 compareTo 方法进行比较来确定元素的优先级,这意味着元素必须实现了 Comparable 接口。
    

2、PriorityBlockingQueue 原理
(1) offer 操作

     在队列中插入一个元素,由于是无界队列,所以一直返回 true。

源码:

public boolean offer(E e) {
        if (e == null)
            throw new NullPointerException();

		// 获取独占锁
        final ReentrantLock lock = this.lock;
        
        lock.lock();
        int n, cap;
        Object[] array;

		// 如果当前元素 >= 队列容量,则扩容
        while ((n = size) >= (cap = (array = queue).length))
        	// (一) 扩容
            tryGrow(array, cap);
            
        try {
            Comparator<? super E> cmp = comparator;
            if (cmp == null)
            	
            	// (二) 建堆
                siftUpComparable(n, e, array);
            else
            	// 自定义比较器
                siftUpUsingComparator(n, e, array, cmp);

			// 队列元素个数加 1 
            size = n + 1;

			// 激活 notEmpty 的条件队列中的一个阻塞线程
            notEmpty.signal();
        } finally {

			// 释放独占锁
            lock.unlock();
        }
        return true;
    }

扩容逻辑

(一):

  private void tryGrow(Object[] array, int oldCap) {
  		// 释放锁
        lock.unlock(); // must release and then re-acquire main lock
        
        Object[] newArray = null;
        
        // CAS 成功则扩容
        if (allocationSpinLock == 0 &&
            UNSAFE.compareAndSwapInt(this, allocationSpinLockOffset,
                                     0, 1)) {
            try {
                int newCap = oldCap + ((oldCap < 64) ?
                                       (oldCap + 2) : // grow faster if small
                                       (oldCap >> 1));
                if (newCap - MAX_ARRAY_SIZE > 0) {    // possible overflow
                    int minCap = oldCap + 1;
                    if (minCap < 0 || minCap > MAX_ARRAY_SIZE)
                        throw new OutOfMemoryError();
                    newCap = MAX_ARRAY_SIZE;
                }
                if (newCap > oldCap && queue == array)
                    newArray = new Object[newCap];
            } finally {
                allocationSpinLock = 0;
            }
        }

		// 第一个线程 CAS 成功后,第二个线程会进入这段代码
        if (newArray == null) // back off if another thread is allocating
        
			// 第二个线程让出 CPU,尽量让第一个线程获取锁,但是这得不到保证
            Thread.yield();

		// 获取锁
        lock.lock();
   
        if (newArray != null && queue == array) {
            queue = newArray;
            System.arraycopy(array, 0, newArray, 0, oldCap);
        }
    }

    可以看到,扩容前先释放锁,❓ 为什么要扩容前先释放了锁,然后使用 CAS 控制 只有一个线程可以扩容成功 ❓其实这里不先释放掉锁,也是可行的,也就是在整个扩容期间一直持有锁,但是扩容是需要花时间的,如果扩容时还占用锁,那么其他线程这时是不能进行入队 和 出队操作的,这大大降低了并发性,所以为了提高性能,使用 CAS 控制 只有一个线程进行扩容,并在扩容前释放锁,让其他线程可以入队和出队。
    具体的扩容方法 是,如果 oldCap < 64,执行 oldCap + 2;否则扩容 50%,并且最大为 MAX_ARRAY_SIZE:private static final int MAX_ARRAY_SIZE = Integer.MAX_VALUE - 8;
    spinlock 锁使用 CAS 控制,只有一个线程可以进行扩容,CAS 失败的线程会调用 Thread.yield() 方法让出 CPU,目的是尽可能让扩容线程扩容后 优先调用 lock.lock() 重新获取锁,(扩容逻辑里,为了提高性能,先释放了锁),但是这得不到保证。有可能 yield 的线程 A 在 扩容线程 B 扩容完成 前已经退出了CPU 的让出,然后线程 A 紧接着获取了锁,而线程 A 的 newArray 仍是 null,所以线程 A 的 tryGrow 就执行完毕了,返回 offer 代码中的 (一)处,继续 while 循环。如果当前数组还没扩容完,当前线程 C 又会调用 tryGrow 方法,然后释放锁,这就又给了线程 B 获取锁的机会,如果这时候线程 B 还没扩容完,CPU 给到线程 C ,线程 C 会调用 Thread.yield 方法交出 CPU。所以 ,当扩容线程进行扩容时,其他线程原地自旋,通过 offer 代码中的 (一) 检查当前扩容是否完毕,扩容完毕后才退出 (一)的循环。
    

建堆算法

(二):

 private static <T> void siftUpComparable(int k, T x, Object[] array) {
        Comparable<? super T> key = (Comparable<? super T>) x;
        while (k > 0) {
            int parent = (k - 1) >>> 1;
            Object e = array[parent];
            if (key.compareTo((T) e) >= 0)
                break;
            array[k] = e;
            k = parent;
        }
        array[k] = key;
    }

    假设队列初始化容量为 2,创建的优先级队列的泛型参数为 Integer。size 为优先级队列中实际有的元素个数;

 /**
     * The number of elements in the priority queue.
     */
    private transient int size;

在这里插入图片描述
    
    先调用队列的 offer(2) ,希望向队列插入元素 2,(n = size) >= (cap = (array = queue).length) 不成立,就走到siftUpComparable 方法中的代码 siftUpComparable(n, e, array); ,由于 k = n =0,因此执行 array[k] = key; ,然后 offer 中会执行 size = n + 1; ,队列个数加 1:

在这里插入图片描述
    
    接下来调用 offer(4) ,希望向队列插入元素 4,(n = size) >= (cap = (array = queue).length) 不成立,就走到siftUpComparable 方法中的代码 siftUpComparable(n, e, array); ,由于 k = 1,因此进入 while 循环,由于 parent = 0, e= 2,key = 4,默认元素比较器使用元素的 compareTo 方法,可知 key > e,所以执行 break 退出循环,然后执行array[k] = key; 然后 offer 中会执行 size = n + 1; ,队列个数加 1:
在这里插入图片描述
    
    接下来调用 offer(6) ,希望向队列插入元素 6,(n = size) >= (cap = (array = queue).length) 成立,调用 tryGrow 进行扩容,由于 就走到siftUpComparable 方法中的代码 siftUpComparable(n, e, array); ,由于 k = 1,因此进入 while 循环,由于 oldCap < 64 ,所以执行 newCap = oldCap + (oldCap+2) = 6,然后创建新数组并复制,之后调用 siftUpComparable 方法,由于 k = 2 > 0, 进入 while 循环,由于 parent = 0, e = 2,key = 6, key > e,所以执行 break 退出 while 循环,并把怨怒是 6 放入数组下标为 2 的地方,最后将 size 值 +1 ,变为 3 :
在这里插入图片描述
    接下来,调用 offer(1),(n = size) >= (cap = (array = queue).length) 不成立,执行 siftUpComparable 方法,由于 k=3,所以进入 while 循环,由于 parent = 1,e = 4,key = 1, key < e,所以把 4 赋给 array[3] ,然后把 1 赋给 k。 再次循环,parent 变成 0,e 变为 2 , key < e,所以把 2 赋给 array[1],然后 把 0 赋给 k,退出 while 循环,最后把 1 赋给 array[0]:
在这里插入图片描述
    这时二叉树堆的树形图:
在这里插入图片描述
    可见,堆的根元素是 1 ,也就是 这是一个最小堆,那么 当调用这个优先级队列的 poll 方法时,会依次返回堆里面最小的值。
    

(2) poll 操作

    获取队列内部堆树的根节点元素,如果队列为空,则返回 null。

源码:

public E poll() {
        final ReentrantLock lock = this.lock;

		// 获取独占锁
        lock.lock();
        
        try {
        	// (一)
            return dequeue();
            
        } finally {
        
        	// 释放独占锁
            lock.unlock();
        }
    }

     可以看到,在出队时要先加锁,这意味着, 当前线程在进行出队操作时,其他线程不能再进行入队和出队操作 ,但是扩容是不影响的。
(一):

   private E dequeue() {
        int n = size - 1;
        
        // 队列为空则返回 null
        if (n < 0)
            return null;
            
        else {
            Object[] array = queue;
            
            // 获取队头元素
            E result = (E) array[0];

			// 获取队尾元素,并赋值 null
            E x = (E) array[n];
            array[n] = null;
            
            Comparator<? super E> cmp = comparator;
            if (cmp == null)
            	// (一)把变量 x 插入到数组下标为 0 的位置,之后重新调整堆 为 最大 或 最小 堆
                siftDownComparable(0, x, array, n);
            else
                siftDownUsingComparator(0, x, array, n, cmp);
            size = n;
            return result;
        }
    }

(一)调整成堆的逻辑:

 private static <T> void siftDownComparable(int k, T x, Object[] array,
                                               int n) {
        if (n > 0) {
            Comparable<? super T> key = (Comparable<? super T>)x;
            int half = n >>> 1;           // loop while a non-leaf
            while (k < half) {
                int child = (k << 1) + 1; // assume left child is least
                Object c = array[child];
                int right = child + 1;
                if (right < n &&
                    ((Comparable<? super T>) c).compareTo((T) array[right]) > 0)
                    c = array[child = right];
                if (key.compareTo((T) c) <= 0)
                    break;
                array[k] = c;
                k = child;
            }
            array[k] = key;
        }
    }

    继续 offer 图解,队列元素的序列为 1、2、6、4。
    第一次调用 poll() 方法时,size = 4 ,n = 3,result = 1, x = 4,然后 把 array[3] 置为 null :
在这里插入图片描述
     然后执行 siftDownComparable ,传入的参数 k = 0, x= 4 .n = 3, 首先 half = 1, key = 4,k < half ,进入 while 循环, child 取值为 1 ,c 取值为 2 , right 取值为 2,把 2 赋给 array[0],1 赋给 k,这时 k 不小于 half ,跳出 while 循环,然后把 4 赋给 array[1] :
在这里插入图片描述
     接下来调用 poll() ,这时 size = 3,n = 2 ,result = 2,x = 6,把队尾元素置为 null :
在这里插入图片描述
    然后执行 siftDownComparable 方法,调整后 :
在这里插入图片描述
    接下来调用 poll() 方法,size = 2,n = 1, result = 4 ,x = 6:
在这里插入图片描述
    然后执行 siftDownComparable 方法,调整后 :
在这里插入图片描述
    调用 poll ,依次出队的顺序是 1、2、4、6。
    下面说说 siftDownComparable 调整堆的算法。思路是,由于队列数组第 0 个元素为树根,因此出队时要移除它 (这个例子是小顶堆)。 这时 数组就不是最小的堆了,需要调整,从被移除的树根的左右子树中 找一个最小的值来当树根,左右子树 又会去找 自己左右子树里的最小值,这是一个递归过程,直到树叶节点结束递归。
    假设当前队列是:
在这里插入图片描述

    其对应的二叉树为:
在这里插入图片描述
    这时如果调用了 poll(),那么 result = 2, x = 11,并且队列末尾的元素被设置为 null ,然后对于剩下的元素,调整堆的步骤如下:
    树根的 左节点 leftChildVal = 4,rightChildVal = 6,由于 4 < 6,所以 c = 4,(c 取小的那个) 然后由于 11 > 4,也就是 key > 4,所以使用元素 4 覆盖树根节点的值,堆变为:
在这里插入图片描述
    然后 树根的左子树 的 左右孩子节点中 leftChildValue = 8,rightChildVal = 10, 由于 8 < 10,所以 c = 8,由于 11 > 8,也就是 key > c,所以元素 8 作为树根左子树的根节点:
在这里插入图片描述

    这时判断是否 k < half,结果为 false,退出循环,然后把 x = 11 的元素设置到数组下标为 3 的地方:
在这里插入图片描述
    这样就调整堆完毕, siftDownCompable 返回的 result 为 2 ,所以 poll 方法也返回了。
    

(3) put 操作

    内部是 offer 操作,源码:

public void put(E e) {
        offer(e); // never need to block
    }

    

(4) take 操作

    获取队列内部堆树的根节点元素,如果队列为空,则阻塞。源码:

public E take() throws InterruptedException {

	// 获取锁
    final ReentrantLock lock = this.lock;

	// 可被中断
    lock.lockInterruptibly();
    
    E result;
    try {
    	// 如果为空则阻塞,把当前线程放入 notEmpty 的条件队列,while 避免虚假唤醒
        while ( (result = dequeue()) == null)
            notEmpty.await();
    } finally {
        lock.unlock();
    }
    return result;
}

    

(5) size 操作

    获取队列元素个数,在返回 size 前加了锁,以保证在调用 size() 方法时不会有其他线程进行入队 和 出队操作。虽然 size 没有用 volatile 修饰,但是加锁保证了内存可见性。

 public int size() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            return size;
        } finally {
            lock.unlock();
        }
    }

    
例👀

import java.util.Random;
import java.util.concurrent.PriorityBlockingQueue;

public class PriorityBlockingQueueTest {

    static class Task implements Comparable<Task>{
        private int priority = 0;
        private String taskName;

        public int getPriority() {
            return priority;
        }

        public void setPriority(int priority) {
            this.priority = priority;
        }

        public String getTaskName() {
            return taskName;
        }

        public void setTaskName(String taskName) {
            this.taskName = taskName;
        }

        /**
         * 自定义元素优先级比较规则
         */
        @Override
        public int compareTo(Task o) {
            if(this.priority >= o.getPriority()){
                return 1;
            }else {
            return -1;
        }
    }

    public void doSomeThing(){
        System.out.println(taskName + ":" + priority);
        }
    }
    public static void main(String[] args) {
        // 创建任务,并添加到队列
        PriorityBlockingQueue<Task> priorityBlockingQueue=new PriorityBlockingQueue<>();
        Random random = new Random();
        for(int i=0;i<10;i++){
            Task task = new Task();
            // 使用随机数生成器生成 10 个随机的有优先级的任务
            task.setPriority(random.nextInt(10));
            task.setTaskName("taskName"+i);
            priorityBlockingQueue.offer(task);
        }
        // 取出任务执行
        while (!priorityBlockingQueue.isEmpty()){
            Task task=priorityBlockingQueue.poll();
            if(null != task){
                task.doSomeThing();
            }
        }

    }
}

运行结果:
在这里插入图片描述
    可以看到,任务执行的先后顺序和它们被放入队列的先后顺序没有关系,而是和它们的优先级有关系。
    
🎭总结:
     PriorityBlockingQueue 队列在内部使用二叉树堆维护元素优先级,使用数组作为元素存储的数据结构,这个数组是可扩容的,当当前元素个数 >= 最大容量时 ,会通过 CAS 算法扩容,出队的是堆树的根节点,使用元素的 compareTo 方法提供默认的元素优先级比较规则,用户可以自定义比较规则。
     PriorityBlockingQueue 类似于 ArrayBlockingQueue,在内部使用一个独占锁来控制同时只有一个线程可以进行入队 和 出队 操作。PriorityBlockingQueue 只使用了 notEmpty 条件变量 而没有使用 notFull,因为 PriorityBlockingQueue 是无界队列,执行 put 操作时 永远不会处于 await 状态,所以也就不需要被唤醒。而 take 方法是阻塞方法,并且是可被中断的。当需要存放有优先级的元素时,可以考虑 PriorityBlockingQueue 。
    

三、 DelayQueue

1、 类图

在这里插入图片描述
    
    可以看到, DelayQueue 内部使用 PriorityQueue 存放数据,使用 ReentrantLock 实现线程同步。队列中的元素要实现 Delay 接口,由于每个元素都有一个过期时间,所以要实现 获知 当前元素还剩下多少时间就过期 的 接口,由于内部使用优先级队列来实现,所以需要实现元素互相比较的接口。
源码:

public interface Delayed extends Comparable<Delayed> {

    long getDelay(TimeUnit unit);
}

     available 和 lock 锁是对应的,其目的是为了实现线程同步。

源码:

private final Condition available = lock.newCondition();

     leader 变量的使用基于 Leader-Follower 模式的变体,用于减少不必要的线程等待。当一个线程调用队列的 take 方法,变为 leader 线程 后,它会调用条件变量 available.awaitNanos (delay) 进行无限等待,而 其他线程(follow 线程)则会调用 available.await() 进行无限等待。leader 线程延迟时间过期后,会退出 take 方法,并通过调用 available.signal() 方法唤醒一个 follow 线程,被唤醒的 follow 线程被选举为新的 leader 线程。
    

2、DelayQueue 原理
(1) offer 操作

     插入元素到队列,如果插入元素为 null 则抛出 NullPointerException 异常 ,否则由于是 无界队列 ,所以一直返回 true。插入元素要实现 Delayed 接口:

 public boolean offer(E e) {
 		
 		// 获取独占锁
        final ReentrantLock lock = this.lock;
        
        lock.lock();
        try {
            q.offer(e);
            
            // 如果当前元素 e 等于队首元素,也就是 最先过期的
            if (q.peek() == e) {
            
            	// 重置 leader 线程为 null 
                leader = null;

				// 激活 available 变量条件队列中的一个线程
                available.signal();
            }
            return true;
        } finally {
            lock.unlock();
        }
    }

    

(2) take 操作

     获取并移除队列里延迟时间过期的元素,如果队列里没有过期元素,则等待。

public E take() throws InterruptedException {
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    try {
    
        for (;;) {
            E first = q.peek();

			// 如果队列为空
            if (first == null)
            	// 把当前线程放入 available 的条件队列里阻塞等待
                available.await();
                
            else {
                long delay = first.getDelay(NANOSECONDS);
                if (delay <= 0)
                    return q.poll();
                first = null; // don't retain ref while waiting

				// 说明其他线程也在执行 take 方法
                if (leader != null)
                    available.await();
                    
                else {
                    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();
    }
}

     可以看到,假设 线程 A 第一次调用 take 方法时 队列为空,就会把当前线程放入 available 的条件队列里阻塞等待。
     当有另一个线程 B 执行了 offer 方法,并添加元素到队列时,假设这时没有其他线程执行入队操作,则 线程 B 添加的元素就是队首元素,在 offer 方法中,q.peek() == e,会重置 leader 为 null,并且激活 available 条件队列里的线程,就是说,线程 A 会被激活,然后线程 A 就会继续执行 for 循环,重新获取队头元素,这时的 first 就是线程 B 新增的元素,这时 first 不为 null,则调用 first.getDelay(NANOSECONDS) 方法查看该元素还剩余多少时间就要过期,如果 delay <= 0 ,则说明已经过期,那么直接出队返回。否则,看 leader 是否为 null,不为 null 说明其他线程也在执行 take 方法(因为 调用 take 方法,就会把当前线程设置为 leader ) ,则把当前线程放入条件队列;如果这时 leader 为 null,则 设置当前线程 A 为 leader 线程,然后等待 delay 时间,这期间该线程会释放锁,所以其他线程可以 offer 添加元素,也可以 take 阻塞自己,剩余过期时间到后,线程 A 再重新竞争得到锁,然后重置 leader 线程为 null,继续循环,这时发现 队头元素以及过期了,就会返回队头元素。 在返回前执行 finally 块里的代码,如果 leader == null && q.peek() != null,就说明 当前线程从队列移除过期元素后,又有其他线程执行了入队操作,那么这时候 调用条件变量的 signal 方法,激活条件队列的等待线程。 (妙!!! 👍 对于可能出现的情况的考虑可以说是面面俱到了👍 )
    

(3) poll 操作

    获取并移除队头过期元素,如果没有过期元素则返回 null 。
源码:

public E poll() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            E first = q.peek();
            if (first == null || first.getDelay(NANOSECONDS) > 0)
                return null;
            else
                return q.poll();
        } finally {
            lock.unlock();
        }
    }

    

(4) size 操作

    计算队列元素个数,包含过期的和没过期的。

public int size() {
        final ReentrantLock lock = this.lock;
        lock.lock();
        try {
            return q.size();
        } finally {
            lock.unlock();
        }
    }

    

👀:

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

/**
   创建一个延迟队列,使用随机数生成 10 个延迟任务,最后依次获取延迟任务,并打印
 */
public class DelayQueueTest {
    static class DelayedEle implements Delayed {

        // 延迟时间,表示 当前任务需要延迟多少 ms 时间 过期
        private final long delayTime;

        // 到期时间
        private final long expire;

        // 任务名称
        private String taskName;

        public DelayedEle(long delay,String taskName){
            delayTime = delay;
            this.taskName = taskName;
            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));
        }

        @Override
        public String toString() {
            return "DelayedEle{" +
                    "delayTime=" + delayTime +
                    ", expire=" + expire +
                    ", taskName='" + taskName + '\'' +
                    '}';
        }

    }
    public static void main(String[] args) {
        // 创建 delay 队列
        DelayQueue<DelayedEle> delayQueue = new DelayQueue<>();

        // 创建延迟任务
        Random random = new Random();
        for(int i = 0;i < 10;++i){
            DelayedEle element = new DelayedEle(random.nextInt(500),"task:" + i);
            delayQueue.offer(element);
        }

        // 依次取出任务并打印
        DelayedEle ele = null;
        try{

            for( ; ;){
                // 获取过期任务并打印
                while ((ele = delayQueue.take()) != null){
                    System.out.println(ele.toString());
                }
            }
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

运行结果:
在这里插入图片描述
🎭总结:
    DelayQueue 内部使用 PriorityQueue 存放数据,使用 ReentrantLock 实现线程同步。队列中的元素要实现 Delayed 接口,其中一个方法 是获取当前元素到过期时间剩余时间的 getDelay,在出队时判断元素是否过期了, Delayed 实现了 Comparable 接口,所以这是个有优先级的队列。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值