想了解更多JUC的知识
——JUC并发编程合集
1. 概述
- 阻塞队列是一个支持两个附加操作的队列。这两个附加的操作是:
- 在队列为空时,获取元素的线程会等待队列变为非空。
- 当队列满时,存储元素的线程会等待队列可用。
- 阻塞队列常用于生产者和消费者的场景,生产者是往队列里添加元素的线程,消费者是从队列里拿元素的线程。阻塞队列就是生产者存放元素的容器,而消费者也只从容器里拿元素
- JDK7提供了7个阻塞队列。分别是
- ArrayBlockingQueue : 一个由数组结构组成的有界阻塞队列。
- LinkedBlockingQueue : 一个由链表结构组成的有界阻塞队列。
- PriorityBlockingQueue : 一个支持优先级排序的无界阻塞队列。
- DelayQueue: 一个使用优先级队列实现的无界阻塞队列。
- SynchronousQueue: 一个不存储元素的阻塞队列。
- LinkedTransferQueue: 一个由链表结构组成的无界阻塞队列。
- LinkedBlockingDeque: 一个由链表结构组成的双向阻塞队列。
- BlockingQueue接口 与 BlockingDeque 接口
- JDK提供的阻塞队列中,LinkedBlockingDeque 是一个 Deque(双向的队列),其实现的接口是 BlockingDeque;其余6个阻塞队列则是 Queue(单向队列),实现的接口是 BlockingQueue。
2. BlockingQueue
2.1 四组API
方法描述 | 抛出异常 | 不抛出异常且有返回值 | 阻塞等待 | 超时等待 |
---|---|---|---|---|
插入数据 | add(e) | offer(e) | put(e) | offer(e,time,timeUnit) |
返回队列头并移除 | remove() | poll() | take() | poll(time,timeUnit) |
返回队列头但不移除 | element() | peek() | - | - |
- 抛出异常: 是指当阻塞队列满时候,再往队列里插入元素,会抛出
IllegalStateException("Queue full")
异常。当队列为空时,从队列里获取元素时会抛出NoSuchElementEx·ception
异常 。 - 有返回值: 插入方法会返回是否成功,成功返回true,失败则返回false。移除方法,则是从队列里拿出一个元素,如果没有则返回null
- 阻塞等待: 当阻塞队列满时,如果生产者线程往队列里put元素,队列会一直阻塞生产者线程,直到数据被取走,或者响应中断退出。当队列空时,消费者线程试图从队列里take元素,队列也会阻塞消费者线程,直到队列中有数据。
- 超时退出: 当阻塞队列满时,队列会阻塞生产者线程一段时间,如果超过一定的时间,生产者线程就会退出。
抛出异常与不抛出异常但有返回值的方法实现是一样的,只不过对失败的处理不一样。通过 AbstractQueue 的源码可以发现,add(e),remove(),element() 都是分别基于 offer(),poll(),peek() 实现的
public boolean add(E e) { if (offer(e)) return true; else throw new IllegalStateException("Queue full"); } public E remove() { E x = poll(); if (x != null) return x; else throw new NoSuchElementException(); } public E element() { E x = peek(); if (x != null) return x; else throw new NoSuchElementException(); }
2.2 其他
- BlockingQueue不接受 null 元素。试图 add、put 或 offer 一个 null 元素时,某些实现会抛出 NullPointerException。null 被用作指示 poll 操作失败的警戒值。
- BlockingQueue可以是限定容量的。它在任意给定时间都可以有一个 remainingCapacity,超出此容量,便无法无阻塞地 put 附加元素。没有任何内部容量约束的 BlockingQueue 总是报告 Integer.MAX_VALUE(约为21亿) 的剩余容量。
- BlockingQueue实现主要用于生产者-使用者队列,但它另外还支持 Collection 接口。因此,举例来说,使用 remove(x) 从队列中移除任意一个元素是有可能的。然而,这种操作通常不会有效执行,只能有计划地偶尔使用,比如在取消排队信息时。
- BlockingQueue实现是线程安全的。所有排队方法都可以使用内部锁或其他形式的并发控制来自动达到它们的目的。然而,大量的 Collection操作(addAll、containsAll、retainAll 和 removeAll,这些方法尽可能地少使用)没有必要自动执行,除非在实现中特别说明。因此,举例来说,在只添加了 c 中的一些元素后,addAll© 有可能失败(抛出一个异常)。
3. BlockingDeque
BlockingDeque是双向队列
,像所有 BlockingQueue 一样,BlockingDeque 是线程安全的,但不允许 null 元素,并且可以有容量限制。- BlockingDeque 接口继承扩展了 BlockingQueue 接口,对于继承自BlockingQueue的方法,除了插入方法(add、poll、offer方法,是插入的队列的尾部),其他方法,操作的都是队列的头部(第一个元素)。
3.1 四组API
- 头部元素
方法描述 | 抛出异常 | 不抛出异常且有返回值 | 阻塞等待 | 超时等待 |
---|---|---|---|---|
插入数据 | addFirst(e) | offerFirst() | putFirst() | offerFirst(e,time,timeUnit) |
返回队列头并移除 | removeFirst() | pollFirst() | takeFirst() | pollFirst(time,timeUnit) |
返回队列头但不移除 | getFirst() | peekFirst() | - | - |
- 尾部元素
方法描述 | 抛出异常 | 不抛出异常且有返回值 | 阻塞等待 | 超时等待 |
---|---|---|---|---|
插入数据 | addLast(e) | offerLast() | putLast() | offerLast(e,time,timeUnit) |
返回队列头并移除 | removeLast() | pollLast() | takeLast() | pollLast(time,timeUnit) |
返回队列头但不移除 | getLast() | peekLast() | - | - |
4. 详细介绍七个阻塞队列
4.1 ArrayBlockingQueue
- ArrayBlockingQueue是一个
用数组实现的有界阻塞队列
。 此队列按照先进先出(FIFO)的原则对元素进行排序。 - 默认情况下不保证访问者公平地访问队列 ,所谓公平访问队列是指阻塞的线程,可按照阻塞的先后顺序访问队列。非公平性是对先等待的线程是不公平的,当队列可用时,阻塞的线程都可以竞争访问队列的资格。
- 为了保证公平性,通常会降低吞吐量。
4.2 LinkedBlockingQueue
- LinkedBlockingQueue是一个用
链表
实现的 有界阻塞队列。此队列的默认和最大长度为Integer.MAX_VALUE(约为21亿)。 此队列按照**先进先出(FIFO)**的原则对元素进行排序。
4.3 PriorityBlockingQueue
-
PriorityBlockingQueue是一个
支持优先级的无界队列
(虽然此队列逻辑上是无界的,但是资源被耗尽时试图执行 add 操作也将失败,导致 OutOfMemoryError)。默认情况下元素采取自然顺序排列(每个元素都必须实现 Comparable 接口),也可以通过比较器comparator来指定元素的排序规则。元素按照升序排列。 -
其iterator() 方法中提供的迭代器并不保证以特定的顺序遍历 PriorityBlockingQueue 的元素。如果需要有序地进行遍历,则应考虑使用Arrays.sort(queue.toArray())。此外,可以使用方法
drainTo
按优先级顺序移除全部或部分元素,并将它们放在另一个collection 中。 -
在此类上进行的操作不保证具有同等优先级的元素的顺序
。如果需要实施某一排序,那么可以定义自定义类或者比较器,比较器可使用修改键断开主优先级值之间的联系。例如,以下是应用先进先出规则断开可比较元素之间联系的一个类。要使用该类,则需要插入一个新的FIFOEntry(anEntry)
来替换普通的条目对象。
4.4 DelayQueue
-
DelayQueue(延时队列),Delayed元素的一个
无界阻塞队列
,只有在延迟期满时才能从中提取元素。注意 DelayQueue 的所有方法只能操作"到期的元素",例如,poll()、remove()、size()等方法,都会忽略掉未到期的元素。 -
我们可以将DelayQueue运用在以下应用场景:
- 缓存系统的设计:可以用DelayQueue保存缓存元素的有效期,使用一个线程循环查询DelayQueue,一旦能从DelayQueue中获取元素时,表示缓存有效期到了。
- 定时任务调度:使用DelayQueue保存当天将会执行的任务和执行时间,一旦从DelayQueue中获取到任务就开始执行,比如TimerQueue就是使用DelayQueue实现的。
-
DelayQueue的实现是
基于PriorityQueue
,是一个优先级队列,是以延时时间的长短进行排序的。所以,DelayQueue需要知道每个元素的延时时间,而这个延时时间是由Delayed接口的getDelay()
方法获取的。所以, DelayQueue的元素必须实现 Delay接口
;//延时队列的原理->源码: public E take() throws InterruptedException { final ReentrantLock lock = this.lock; lock.lockInterruptibly(); try { for (;;) { E first = q.peek();//从优先队列中获取第一个元素 if (first == null) available.await();//若获取当前元素的为null(即为空),则进入等待阻塞状态 else {//当前元素不为null long delay = first.getDelay(NANOSECONDS);//获取当前元素的延时时间 if (delay <= 0)//延时时间到期 return q.poll();//调用poll方法移除该元素并返回 first = null; // don't retain ref while waiting //leader表示其他线程,leader不为空的意思是已经有其他队列在等待这个延时对象了 if (leader != null) //让当前线程进入等待阻塞状态,避免延时相同时间造成大量线程同时唤醒 available.await(); else { //若leader为空,则指定当前线程为leader Thread thisThread = Thread.currentThread(); leader = thisThread; try { //让当前线程等待阻塞delay时间(纳秒级别)后主动唤醒,然后去竞争锁。 //如果竞争成功则大概率可以获取到延时结点 //如果竞争失败,则进入同步队列阻塞 available.awaitNanos(delay); } finally { //leader线程被唤醒并获取到锁之后会将leader设置为空 if (leader == thisThread) leader = null; } } } } } finally { //leader为空并且队列不为空,那么唤醒正在等待的线程 if (leader == null && q.peek() != null) available.signal(); lock.unlock(); } }
4.5 SynchronousQueue
-
SynchronousQueue也是一个阻塞队列,但它的特别之处在于它内部没有容器。一个生产线程,当它生产产品(即put的时候),如果当前没有人想要消费产品(即当前没有线程执行take),此生产线程必须阻塞,等待一个消费线程调用take操作,take操作将会唤醒该生产线程,同时消费线程会获取生产线程的产品(即数据传递),这样的一个过程称为一次配对过程(先take后put,原理相似)。
-
特点:
-
同步队列没有任何内部容量,甚至连一个队列的容量都没有。 所以很多继承的方法就没有用了,(如 isEmpty()始终返回true,size()始终为0,包含contain、移除remove都始终为false等等)。或者说,真正有意义的只有以下几个方法:获取并移除(poll()、poll(timeout,timeunit)、take())、插入(offer()、offer(timeout,timeunit)、put());
-
适合于传递性设计,在这种设计中, 每一个put操作必须等待一个take操作,反之亦然 。(当然,如果用的是offer、poll的话,那么就不会阻塞等待)。SynchronousQueue可以看成是一个传球手,负责把生产者线程处理的数据直接传递给消费者线程。
-
支持可选的公平排序策略。 默认情况下不保证这种排序。但是,使用公平设置为 true 所构造的队列可保证线程以 FIFO 的顺序进行访问。
-
4.6 LinkedTransferQueue
- LinkedTransferQueue是一个由
链表结构组成的无界阻塞
TransferQueue队列 。相对于其他阻塞队列LinkedTransferQueue多了tryTransfer
和transfer
方法。- transfer方法: 如果当前有消费者正在等待接收元素(消费者使用take()方法或带时间限制的poll()方法时),transfer方法可以把生产者传入的元素立刻transfer(传输)给消费者。如果没有消费者在等待接收元素,transfer方法会将元素存放在队列的tail节点,并等到该元素被消费者消费了才返回。
- tryTransfer方法: 则是用来试探下生产者传入的元素是否能直接传给消费者。如果没有消费者等待接收元素,则返回false。
- tryTransfer(E e, long timeout, TimeUnit unit)方法 :试图把生产者传入的元素直接传给消费者,但是如果没有消费者消费该元素则等待指定的时间再返回,如果超时还没消费元素,则返回false,如果在超时时间内消费了元素,则返回true
- transfer方法和不带参数的tryTransfer方法的区别是tryTransfer方法无论消费者是否接收,方法立即返回。而transfer方法是必须等到消费者消费了才返回。
4.7 LinkedBlockingDeque
-
LinkedBlockingDeque是一个由
链表结构组成的双向阻塞队列
,所谓双向队列指的你可以从队列的两端插入和移出元素,默认长度以及最大长度是 Integer.MAX_VALUE(约为21亿),可在创建时,指定容量。 -
双端队列因为多了一个操作队列的入口,在多线程同时入队时,也就减少了一半的竞争。
-
相比其他的阻塞队列,LinkedBlockingDeque增加了对头元素和尾元素的操作,使用时用带有First和Last后缀的方法使得操作更清楚。