13、JUC阻塞队列(BlockingQueue、ArrayBlockingQueue、PriorityBlockingQueue、DelayQueue)

一、BlockingQueue

1.1 什么是阻塞队列?

阻塞队列(BlockingQueue)是一个支持两个附加操作的队列。这两个附加的操作是:

  • 在队列为空时,获取元素的线程会等待队列变为非空。
  • 当队列满时,存储元素的线程会等待队列可用。

阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素。

阻塞队列提供了四种处理方法:

方法\处理方式 抛出异常 返回特殊值 一直阻塞 超时退出
插入方法 add(e) offer(e) put(e) offer(e,time,unit)
移除方法 remove() poll() take() poll(time,unit)
检查方法 element() peek() 不可用 不可用
  • 异常:是指当阻塞队列满时候,再往队列里插入元素,会抛出IllegalStateException(“Queue full”)异常。当队列为空时,从队列里获取元素时会抛出NoSuchElementException异常 。
  • 返回特殊值:插入方法会返回是否成功,成功则返回true。移除方法,则是从队列里拿出一个元素,如果没有则返回null
  • 一直阻塞:当阻塞队列满时,如果生产者线程往队列里put元素,队列会一直阻塞生产者线程,直到拿到数据,或者响应中断退出。当队列空时,消费者线程试图从队列里take元素,队列也会阻塞消费者线程,直到队列可用。
  • 超时退出:当阻塞队列满时,队列会阻塞生产者线程一段时间,如果超过一定的时间,生产者线程就会退出。

1.2 API介绍

阻塞队列接口:

public interface BlockingQueue<E> extends Queue<E> {
   
     
	//插入元素e到队列中,成功返回true, 否则抛出异常。如果向限定了容量的队列中插入值,推荐使用offer()方法。
	boolean add(E e);
	
	//插入元素e到队列中,如果设置成功返回true, 否则返回false. e的值不能为空,否则抛出空指针异常。
	boolean offer(E e);
	
	//插入元素e到队列中,,如果队列中没有多余的空间,该方法会一直阻塞,直到队列中有多余的空间。
	void put(E e) throws InterruptedException;
	
	//在给定的时间插入元素e到队列中,如果设置成功返回true, 否则返回false.
	boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException;
	
	//检索并从队列的头部删除元素,如果队列中没有值,线程会一直阻塞,直到队列中有值,并且该方法取得了该值。
	E take() throws InterruptedException;
	
	//在给定的时间范围内,检索并从队列的头部删除元素,从队列中获取值,如果没有取到会返回null
	E poll(long timeout, TimeUnit unit) throws InterruptedException;
	
	//获取队列中剩余的空间。
	int remainingCapacity();
	
	//从队列中移除指定的值。
	boolean remove(Object o);
	
	//判断队列中是否包含该值。
	public boolean contains(Object o);
	
	//将队列中值,全部移除,并追加到给定的集合中。
	int drainTo(Collection<? super E> c);
	
	//指定最多数量限制将队列中值,全部移除,并追加到给定的集合中。
	int drainTo(Collection<? super E> c, int maxElements);
}

1.3 子接口

BlockingDeque

TransferQueue

TransferQueue继承了BlockingQueue,并扩展了一些新方法。
BlockingQueue是指这样的一个队列:当生产者向队列添加元素但队列已满时,生产者会被阻塞;当消费者从队列移除元素但队列为空时,消费者会被阻塞。

TransferQueue则更进一步,生产者会一直阻塞直到所添加到队列的元素被某一个消费者所消费(不仅仅是添加到队列里就完事)。新添加的transfer方法用来实现这种约束。顾名思义,阻塞就是发生在元素从一个线程transfer到另一个线程的过程中,它有效地实现了元素在线程之间的传递(以建立Java内存模型中的happens-before关系的方式)。

【并发重要原则】happens-before理解和应用

TransferQueue还包括了其他的一些方法:两个tryTransfer方法,一个是非阻塞的,另一个带有timeout参数设置超时时间的。还有两个辅助方法hasWaitingConsumer()和getWaitingConsumerCount()。

1.4 实现类

JUC一共提供了7种实现类,我们会依次讲解:
ArrayBlockingQueue
PriorityBlockingQueue
DelayQueue
LinkedBlockingQueue
LinkedBlockingDeque
SynchronousQueue
LinkedTransferQueue

二、ArrayBlockingQueue

2.1 API介绍

ArrayBlockingQueue 是一个线程安全的、基于数组、有界的、阻塞的、FIFO 队列。试图向已满队列中放入元素会导致操作受阻塞;试图从空队列中提取元素将导致类似阻塞。

此类基于 java.util.concurrent.locks.ReentrantLock 来实现线程安全,所以提供了 ReentrantLock 所能支持的公平性选择。

2.2 源码简析

底层是通过ReentrantLock,和Condition实现的阻塞、唤醒,原理比较简单:

以put方法添加元素为例:

队列空的话取元素也会阻塞:

dequeue是出队方法:

2.3 案例演示

public class ArrayBlockingQueueDemo {
   
     
	public static void main(String[] args) {
   
     
	
		BlockingQueue<Integer> blockingQueue = new ArrayBlockingQueue<Integer>(3,true);
		Producer producer = new Producer(blockingQueue);
		Consumer consumer = new Consumer(blockingQueue);
		new Thread(producer).start();
		new Thread(consumer).start();
		
	}
}

class Producer implements Runnable {
   
     

	private BlockingQueue<Integer> blockingQueue;
	private static int element = 0;
	
	public Producer(BlockingQueue<Integer> blockingQueue) {
   
     
		this.blockingQueue = blockingQueue;
	}
	
	public void run() {
   
     
		try {
   
     
			while(element < 20) {
   
     
				System.out.println("生产元素:"+element);
				blockingQueue.put(element++);
			}
		} catch (Exception e) {
   
     
			System.out.println("生产者在等待空闲空间的时候发生异常!");
			e.printStackTrace();
		}
		System.out.println("生产者终止了生产过程!");
	}
}

class Consumer implements Runnable {
   
     

	private BlockingQueue<Integer> blockingQueue;
	
	public Consumer(BlockingQueue<Integer> blockingQueue) {
   
     
		this.blockingQueue = blockingQueue;
	}
	
	public void run() {
   
     
		try {
   
     
			while(true) {
   
     
				System.out.println("消费元素:"+blockingQueue.take());
			}
		} catch (Exception e) {
   
     
			System.out.println("消费者在等待新产品的时候发生异常!");
			e.printStackTrace();
		}
		System.out.println("消费者终止了消费过程!");
	}
}

三、PriorityBlockingQueue

3.1 API介绍

PriorityBlockingQueue是带优先级的无界阻塞队列,每次出队都返回优先级最高的元素,是二叉树最小堆的实现。

  • 无界阻塞队列,并不是无界而是说在容量快满的时候会自动扩容。
  • 不允许空元素,并且添加的元素需要实现Comparable接口,实现自然排序。
  • 不能保证优先级一样的元素的顺序,需要用额外属性进行排序。

算法复杂度中的O(logN)底数是多少

3.2 源码简析

底层数据结构是二叉堆

public class PriorityBlockingQueue<E> extends AbstractQueue<E> implements BlockingQueue<E>, java.io.Serializable {
   
     
	...
	
	//可以看到底层是用平衡二叉堆存储元素的。
    /**
     * Priority queue represented as a balanced binary heap: the two
     * children of queue[n] are queue[2*n+1] and queue[2*(n+1)].  The
     * priority queue is ordered by comparator, or by the elements'
     * natural ordering, if comparator is null: For each node n in the
     * heap and each descendant d of n, n <= d.  The element with the
     * lowest value is in queue[0], assuming the queue is nonempty.
     */
    private transient Object[] queue;
    ...
}

关于二叉堆这里就不细说了。

扩容逻辑

//扩容的时候会调用
/**
 * Tries to grow array to accommodate at least one more element
 * (but normally expand by about 50%), giving up (allowing retry)
 * on contention (which we expect to be rare). Call only while
 * holding lock.
 *
 * @param array the heap array
 * @param oldCap the length of the array
 */
private void tryGrow(Object[] array, int oldCap) {
   
     
    lock.unlock(); // must release and then re-acquire main lock
    Object[] newArray = null;
    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;
        }
    }
    if (newArray == null) // back off if another thread is allocating
        Thread.yield();
    lock.lock();
    if (newArray != null && queue == array) {
   
     
        queue = newArray;
        System.arraycopy(array, 0, newArray, 0, oldCap);
    }
}

获取元素

//出队的操作,用二叉堆的下浮操作
/**
 * Mechanics for poll().  Call only while holding lock.
 */
private E dequeue() {
   
     
    int n = size - 1;
    if (n < 0)
        return null;
    else {
   
     
        Object[] array = queue;
        E result = (E) array[0];//二叉队出队逻辑是弹出顶端元素
        E x = (E) array[n];//将最后一个元素放到顶端,执行下浮操作
        array[n] = null;
        Comparator<? super E> cmp = comparator;
        if (cmp == null)//使用比较器排序执行下浮操作
            siftDownComparable(0, x, array, n);
        else//使用自然排序执行下浮操作
            siftDownUsingComparator(0, x, array, n, cmp);
        size = n;
        return result;
    }
}

下浮操作是二叉堆移除元素的算法,本节不细究。

//可以看到take就是调用dequeue出队方法获取元素的
public E take() throws InterruptedException {
   
     
    final ReentrantLock lock = this.lock;
    lock.lockInterruptibly();
    E result;
    try {
   
     
        while ( (result = dequeue()) == null)//通过dequeue方法获取元素,如果队列为空就会进行一个阻塞操作
            notEmpty.await();
    } finally {
   
     
        lock.unlock();
    }
    return result;
}

添加元素

//追加元素,用上浮操作
/**
 * Inserts the specified element into this priority queue.
 * As the queue is unbounded, this method will never return {@code false}.
 *
 * @param e the element to add
 * @return {@code true} (as specified by {@link Queue#offer})
 * @throws ClassCastException if the specified element cannot be compared
 *         with elements currently in the priority queue according to the
 *         priority queue's ordering
 * @throws NullPointerException if the specified element is null
 */
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);
        size = n + 1;//插入成功,容量大小+1
        notEmpty.signal();//发一个信号,如果有线程在等待队列里的元素,做一个唤醒。
    } finally {
   
     
        lock.unlock();
    }
    return true;
}

上浮操作是二叉堆的添加元素算法,本节不细究。

//可以看到put、offer(timeout)都调用的的offer,因为这个队列是无界的
//所以不会产生因为队列满而阻塞的情况
public void put(E e) {
   
     
    offer(e); // never need to block
}
public boolean offer(E e, long timeout, TimeUnit unit) {
   
     
    return offer(e); // never need to block
}

3.3 案例演示

public class PriorityBlockingQueueTest {
   
     

	public static void main(String[] args) throws InterruptedException {
   
     
		PriorityBlockingQueue<PriorityElement> queue = new PriorityBlockingQueue<>();
		
		for (int i = 0; i < 5; i++) {
   
     
			Random random=new Random();
			PriorityElement ele = new PriorityElement(random.nextInt(10));
			queue.put(ele);
		}
		
		while(!queue.isEmpty()){
   
     
			System.out.println(queue.take());
		}
		
	}
}

class PriorityElement implements Comparable<PriorityElement> {
   
     

	private int priority;//定义优先级
	
	PriorityElement(int priority) {
   
     
		//初始化优先级
		this.priority = priority;
	}
	
	@Override
	public int compareTo(PriorityElement o) {
   
     
		//按照优先级大小进行排序
		return priority >= o.getPriority() ? 1 : -1;
	}
	
	public int getPriority() {
   
     
		return priority;
	}
	
	public void setPriority(int priority) {
   
     
		this.priority = priority;
	}
	
	@Override
	public String toString() {
   
     
		return "PriorityElement [priority=" + priority + "]";
	}
}

四、DelayQueue

4.1 API介绍

DelayQueue队列中每个元素都有个过期时间,并且队列是个优先级队列,当从队列获取元素时候,只有过期元素才会出队列。

应用场景:

  • 比如买东西下订单,一天之内没有付款就会关闭,或者秒杀业务,有时间限制等,就可以使用这个延迟队列。
  • 比如连接服务器,服务器会监测到很多连接是空闲的,这个时候超过一定时间也可以把空闲连接进行关闭。

三个特点:无界、阻塞、过期、只能取到队首的过期元素

Delayed 接口

放入DelayQueue的元素必须实现Delayed接口

Delayed继承自Comparable,所以也需要实现自然排序。

4.2 源码简析

底层数据结构是PriorityQueue

public class DelayQueue<E extends Delayed> extends AbstractQueue<E> implements BlockingQueue<E> {
   
     
	//看到底层用带优先级的队列来存储元素
    private final PriorityQueue<E> q = new PriorityQueue<E>();
	...
}

等待优化

public class DelayQueue<E extends Delayed> extends AbstractQueue<E> implements BlockingQueue<E> {
   
     
	...
    /**
     * 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.
     * 
     * 指定等待队列头元素的线程。这个Leader-Follower模式的变体(http://www.cs.wustl.edu/~schmidt/POSA/POSA2/)
     * 可以将不必要的时间等待最小化。当一个线程成为leader时,它只等待下一次延迟,而其他线程则无限期地等待。
     * 在从take()或 poll(…)返回之前,领头线程必须向其他线程发出信号,除非其他线程在过渡期间成为领头线程。
     * 当队列的头被具有较早过期时间的元素替换时,leader字段将被重置为null,并向一些等待的线程发出信号,但
     * 不一定是当前的leader。因此,等待线程必须准备好在等待时获得或失去领导权。
     */
    private Thread leader = null;

    /**
     * Condition signalled when a newer element becomes available
     * at the head of the queue or a new thread may need to
     * become leader.
     * 当一个新的元素在队列的最前面可用或者一个新的线程需要成为leader时,发出信号的条件。
     */
    private final Condition available = lock.newCondition();
    ...
}

DelayQueue做了一个优化,如果一个线程获取队首元素,但是该元素还没有过期,那么会将当前线程记录到leader变量上,让其阻塞到该元素刚好过期的时间,期间其他线程获取元素的时候会直接无限期等待。这样可以将不必要的时间等待最小化。

如果leader等待过程中添加了更早的的元素,会做相应的唤醒等一系列处理。

获取元素

//获取元素的方法
/**
 * 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 {
   
     
                long delay = first.getDelay(NANOSECONDS);
                if (delay <= 0)//判断是否过期
                    return q.poll();//过期直接出队返回
                first = null; // don't retain ref while waiting
                if (leader != null)
                    available.await();//leader不为null,说明已经有其他线程比自己先等待了,当前线程就无限期阻塞
                else {
   
     
                 	//空的话说明没有其他线程在等待
                    Thread thisThread = Thread.currentThread();
                    //当前线程变为leader
                    leader = thisThread;
                    try {
   
     
                        available.awaitNanos(delay);//使当前线程等待,直到发出信号或中断,或者指定的等待时间过去。
                        //等待时间刚好是元素过期的时间
                    } finally {
   
     
                        if (leader == thisThread)
                            leader = null;//释放leader
                    }
                }
            }
        }
    } finally {
   
     
        if (leader == null && q.peek() != null)
            available.signal();
        lock.unlock();
    }
}

添加元素

//追加元素
/**
 * 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 {
   
     
        q.offer(e);
        if (q.peek() == e) {
   
     
         	//成立说明刚添加的元素是队列里面延迟时间最短的元素
         	//刷新了最短记录,这个时候需要把leader情况,然后唤醒所有等待的线程
         	//重新执行获取元素的逻辑
            leader = null;
            available.signal();
        }
        return true;
    } finally {
   
     
        lock.unlock();
    }
}

4.3 案例演示

public class DelayQueueTest  {
   
     

    public static void main(String[] args) throws InterruptedException {
   
     
        Item item1 = new Item("item1", 5, TimeUnit.SECONDS);
        Item item2 = new Item("item2",10, TimeUnit.SECONDS);
        Item item3 = new Item("item3",15, TimeUnit.SECONDS);
        DelayQueue<Item> queue = new DelayQueue<>();
        queue.put(item1);
        queue.put(item2);
        queue.put(item3);
        System.out.println("begin time:" + LocalDateTime.now().format(DateTimeFormatter.ISO_LOCAL_DATE_TIME));
        for (int i = 0; i < 3; i++) {
   
     
            Item take = queue.take();//阻塞获取
            System.out.format("name:{%s}, time:{%s}\n",take.name, LocalDateTime.now().format(DateTimeFormatter.ISO_DATE_TIME));
        }
    }

}

class Item implements Delayed {
   
     
    /* 触发时间*/
    private long time;
    String name;

    public Item(String name, long time, TimeUnit unit) {
   
     
        this.name = name;
        this.time = System.currentTimeMillis() + (time > 0? unit.toMillis(time): 0);
    }

    @Override
    public long getDelay(TimeUnit unit) {
   
     
        return time - System.currentTimeMillis();
    }

    @Override
    public int compareTo(Delayed o) {
   
     
        Item item = (Item) o;
        long diff = this.time - item.time;
        if (diff <= 0) {
   
     // 改成>=会造成问题
            return -1;
        }else {
   
     
            return 1;
        }
    }

    @Override
    public String toString() {
   
     
        return "Item{" +
                "time=" + time +
                ", name='" + name + '\'' +
                '}';
    }
}