概述
接下来开始学习java.util.concurrent包中一些Collection集合的子类,关于Map的一些子类将在这些子类完成之后再开始学习。下图是Java并发包中关于Collection接口的一些实现的关系类图,当然为了简化复杂度,我没有就所有的父子关系都列出来,而是仅仅就一些特殊的关系画了出来,例如ConcurrentSkipListSet 不仅继承了AbstractSet,它还实现了NavigableSet、Cloneable, java.io.Serializable接口。Note: 绿色表示接口,蓝色表示实例类,黄色表示抽象类。
上面的类结构图可以分为三个大部分,分别是Set、List、Queue,双向Queue即Deque只是Queue的一种特殊实现。上图左上角是Set相关的类图,右上角是List相关的类图(仅仅只有一个CopyOnWriteArrayList),右下角是双向队列Deque的相关类图,中下部分都是队列Queue的类图。与对列Queue相关的类占据了JUC并发包的大部分内容。
java.util.Queue
作为Collection的直接子接口,Queue的作用当然也是用于存取数据,除了拥有Collection的基本操作,Queue提供了一系列额外的用于插入、提取和元素检测操作。这些方法都以两种形式存在:①如果操作失败,则抛出异常;② 返回一个特殊值(null或false,取决于具体操作)。后一种形式的插入操作专门设计用于对容量有限制的队列的操作。
以下六个接口方法就是Queue接口定义的所有方法:
操作失败抛异常 | 操作失败返回特殊值 | |
---|---|---|
插入 | boolean add(E e) | boolean offer(E e) |
获取并且移除 | E remove() | E poll() |
获取但不移除 | E element() | E peek() |
插入方法add(e)和offer(e) --- add方法尽管有个boolean返回值,但是其实只有成功才会返回true,失败会直接抛出异常;offer方法才被设计成失败返回false,成功返回true。
获取并移除方法remove和poll --- 都是返回并删除队列的头节点元素,不同的是在队列为空时,remove抛出异常,poll返回null。
获取不移除方法element和peek --- 都是返回但是不删除队列的头节点元素,不同的是在队列为空时,element抛出异常,peek返回null。
关于插入null值,队列实现通常不允许插入空元素,虽然有些实现,如LinkedList,不禁止插入null。即使在允许null的实现中,也不应该将null插入到队列中,因为 poll和peek 方法将null用作特殊的返回值,以指示队列不包含元素。
关于hashCode和equals方法,队列实现通常不定义基于元素版本的equals和hashCode方法,而是从Object类继承基础版本,因为对于具有相同元素但元素在集合中的排序顺序的情况不总是友好的。
关于元素排序,队列通常(但并非一定)以 FIFO(先进先出)的方式排序各个元素。例如优先级队列PriorityQueues,它根据提供的比较器或元素的自然顺序对元素进行排序;LIFO的队列(例如堆栈)按后进先出的方式排队元素。无论使用哪种排序方式,队列的头都是调用 remove() 或 poll() 所移除的元素。在 FIFO 队列中,所有的新元素都插入队列的末尾。其他种类的队列可能使用不同的元素放置规则。每个Queue的实现必须指定其元素排序方式。
关于阻塞方法,Queue接口中并没有定义这类接口,它们在Queue子接口BlockingQueue接口中定义。
Java.util.AbstractQueue
通过上面的类图可以发现,几乎Queue的所有实现(除了ConcurrentLinkedDeque)都继承了AbstractQueue抽象类,通过查看其源码可以发现,AbstractQueue抽象类其实就是利用了Queue定义的offer、poll、peek方法分别重新实现了add、remove、element方法,但是AbstractQueue的这种重写实现并没有改变add、remove、element方法定义的最终返回结果,在队列满时add方法照样抛出异常,在队列为空时,remove和element方法照样抛出异常。
AbstractQueue类还新增了两个方法,分别是基于poll的clear方法,以及基于add方法的addAll方法。AbstractQueue并没有声明任何抽象方法。
BlockingQueue
该接口继承Queue接口,就是JUC并发包队列的基础接口,它定义了一系列阻塞方法,以支持在获取元素时等待队列变为非空,并在存储元素时等待队列的空间可用。BlockingQueue接口在Queue的基础上将Queue存在两种形式的方法扩展到拥有四种形式,即对于不能立即满足但是在将来的某个时候可能会满足的操作的不同处理方式:1.抛出异常。2.返回一个特殊值(null或false,取决于操作)。3.线程无限期地阻塞当前线程,直到操作成功或被打断。4.等待给定的最大时间限制,直到操作成功或超时或被打断。总结这些方法如下:
操作失败抛异常 | 操作失败返回特殊值 | 条件不满足阻塞 | 条件不满足等待给定的时间 | |
---|---|---|---|---|
插入 | boolean add(E e) | boolean offer(E e) | void put(E e) throws InterruptedException |
boolean offer(E e, long timeout, TimeUnit unit) throws InterruptedException |
获取并且移除 | boolean remove(Object o) | E poll() | E take() throws InterruptedException |
E poll(long timeout, TimeUnit unit) throws InterruptedException |
获取但不移除 | E element() | E peek() | 不可用 | 不可用 |
表格的前两种类型和Queue接口定义的接口方法一致,但是除了绿色单元格中的方法是直接继承自Queue,其它三个方法其实都被BlockingQueue重新声明了,但是没有改变这些方法的涵义。后面两种类型的方法是BlockingQueue新增的对阻塞的支持方法。
BlockingQueue不接受插入null对象。当尝试add,put或offer一个null时将抛出NullPointerException。null被作为poll/peek方法操作失败之后的返回值。
BlockingQueue新增了一个int remainingCapacity()方法,用于返回当前队列剩余可用的容量,因为BlockingQueue可能是有容量限制的。没有容量限制的BlockingQueue该方法总是返回Integer.MAX_VALUE。
BlockingQueue还新增了两个drainTo方法,用于从当前队列中移除全部或者指定数量的元素到参数指定的集合中。
BlockingQueue实现是线程安全的。所有队列方法都使用内部锁或其他并发控制形式以到达它们的原子效果。但是,除非在实现中特殊说明,否则批量集合操作addAll、containsAll、retainAll和removeAll不一定以原子方式执行。
内存一致性:
线程在将对象放入 BlockingQueue队列之前的操作 happen-before 另一个线程从BlockingQueue获取或者移除该元素之后的操作。
Java.util.Deque
一种支持从两端插入和移除元素的线性集合。deque是“double ended queue(双端队列)”的缩写,虽然大多数的Dqueu实现都是无界的,但事实上该接口也支持有界的双端队列实现。该接口定义了从双端队列两端访问元素的方法,这样方法同Queue接口一样都是用于对元素的插入、提取和检测操作,每种方法也都存在两种形式:①操作失败抛异常;②返回特殊值(null或false,取决于具体操作)。后一种形式的插入操作专门设计用于对容量有限制的队列的操作。这些方法有如下12个:
第一个元素(即头部) | 最后一个元素(即尾部) | |||
失败抛异常 | 失败返回特殊值 | 失败抛异常 | 失败返回特殊值 | |
插入 | addFirst(e) | offerFirst(e) | addLast(e) | offerLast(e) |
移除 | removeFirst() | pollFirst() | removeLast() | pollLast() |
获取但不移除(即检查) | getFirst() | peekFirst() | getLast() | peekLast() |
该接口扩展了Queue接口,将双端队列当着简单队列使用时,它是先进先出的队列,即在双端队列尾部添加新元素,从双端队列头部移除元素,所以Deque其实也具备Queue的特性,凡是Deque中从Queue继承的方法都是按FIFO队列实现,它们定义的接口方法有些是等效的:
操作 | Queue方法 | 等效的Deque方法 |
在对尾插入 | add(e) | addLast(e) |
offer(e) | offerLast(e) | |
在队头移除 | remove() | removeFirst() |
poll() | pollFirst() | |
在对头获取但不移除 | element() | getFirst() |
peek() | peekFirst() |
同样的,双端队列也可以当着后进先出的堆栈使用,即在双端队列的头部进行添加和移除操作,所以Deque其实也具备Stack(堆栈)的特性,它们定义的接口方法也有些是等效的:
操作 | 堆栈方法 | 等效的Deque方法 |
在对头插入 | push(e) | addFirst(e) |
在对头移除 | pop() | removeFirst() |
在对头获取但不移除 | peek() | peekFirst() |
无论将双端队列用作普通的先进先出FIFO队列还是后进先出LIFO的堆栈使用,peek()方法的语义都不变,即都是从双端队列的头部获取(但不移除)元素。
此接口还额外新增加了移除内部元素的方法:removeFirstOccurrence(Object o) 和 removeLastOccurrence(Object o),它们分别表示移除双端队列中第一次或最后一次出现的指定元素。
与List接口不同,此接口不支持通过索引访问队列元素。另外,虽然双端队列Deque没有严格要求禁止插入null元素,但是依然不建议这样做,即使某些Deque的实现支持插入null,因为有一些方法会将null作为特殊的返回值来指示双端队列为空。
关于hashCode和equals方法,双端队列实现通常也不定义基于元素版本的equals和hashCode方法,而是从Object类继承基础版本。
BlockingDeque
类似Queue对应的BlockingQueue,Deque也有与其对应的同步阻塞接口BlockingDeque,它是一种附加了支持阻塞操作的双端队列,即获取元素时等待双端队列变为非空,存储元素时等待双端队列中的空间变得可用。类似BlockingQueue,BlockingDeque也在Deque的基础上扩展出了两种形式的方法,即条件不满足时无限期阻塞直到成功和条件不满足时等待给定的超时时长:
第一个元素(即头部) | 最后一个元素(即尾部) | |||
条件不满足时阻塞 | 条件不满足时等待超时 | 条件不满足时阻塞 | 条件不满足时等待超时 | |
插入 | putFirst(e) | offerFirst(e, time, unit) | putLast(e) | offerLast(e, time, unit) |
移除 | takeFirst() | pollFirst(time, unit) | takeLast() | pollLast(time, unit) |
获取但不移除(即检查) | 不适用 | 不适用 | 不适用 | 不适用 |
从列表可见,BlockingDeque不对元素检查(即获取但不移除)做任何更改,像所有 BlockingQueue 一样,BlockingDeque也是线程安全的,但不允许插入null 元素,并且可能有(也可能没有)容量限制。
其实BlockingDeque还继承了BlockingQueue ,只是最上面的类结构图没有画出来,所以BlockingDeque也是一种BlockingQueue,它可以直接被当成FIFO的BlockingQueue使用。继承自 BlockingQueue 接口的方法精确地等效于下表中描述的 BlockingDeque 方法:
操作 | BlockingQueue 方法 | 等效的 BlockingDeque 方法 |
插入 | add(e) | addLast(e) |
offer(e) | offerLast(e) | |
put(e) | putLast(e) | |
offer(e, time, unit) | offerLast(e, time, unit) | |
移除 | remove() | removeFirst() |
poll() | pollFirst() | |
take() | takeFirst() | |
poll(time, unit) | pollFirst(time, unit) | |
获取但不移除(即检查) | element() | getFirst() |
peek() | peekFirst() |
内存一致性效果:
与其它并发集合一样,将对象放入 BlockingDeque
之前的线程中的操作 happen-before 随后通过另一线程从 BlockingDeque
中访问或移除该元素的操作。