Java中Queue接口的学习
Queue接口
非阻塞队列
LinkedList
在Java中,Deque(双端队列)是一个接口,而LinkedList类实现了Deque接口,因此它也可以被用作双端队列。
特点
- 双端操作:
LinkedList作为Deque的实现,支持从两端添加、删除和访问元素。 - 动态扩容:其容量可以根据需要动态增长,无需预先指定大小。
- 非线程安全:
LinkedList不是线程安全的,如果需要在多线程环境下使用,需要进行外部同步。 - 允许null元素:
LinkedList允许包含null元素。
底层结构
- 链表结构:
LinkedList的底层是通过双向链表实现的。每个节点(Node)都包含三个部分:元素值(item)、指向前一个节点的引用(prev)和指向后一个节点的引用(next)。 - 头尾指针:
LinkedList维护了两个指针,分别指向链表的头部(first)和尾部(last),以便于从两端进行操作。
常用用法
作为Deque的实现,LinkedList提供了丰富的双端队列操作方法:
-
添加元素:
addFirst(E e):在双端队列的头部添加元素。addLast(E e):在双端队列的尾部添加元素。offerFirst(E e):在双端队列的头部添加元素,并返回true(因为总是可以添加)。offerLast(E e):在双端队列的尾部添加元素,并返回true(因为总是可以添加)。
-
删除元素:
removeFirst():删除并返回双端队列头部的元素,如果队列为空,则抛出NoSuchElementException。removeLast():删除并返回双端队列尾部的元素,如果队列为空,则抛出NoSuchElementException。pollFirst():删除并返回双端队列头部的元素,如果队列为空,则返回null。pollLast():删除并返回双端队列尾部的元素,如果队列为空,则返回null。
-
获取元素:
getFirst():获取但不删除双端队列头部的元素,如果队列为空,则抛出NoSuchElementException。getLast():获取但不删除双端队列尾部的元素,如果队列为空,则抛出NoSuchElementException。peekFirst():获取但不删除双端队列头部的元素,如果队列为空,则返回null。peekLast():获取但不删除双端队列尾部的元素,如果队列为空,则返回null。
-
其他操作:
isEmpty():检查双端队列是否为空。size():返回双端队列中的元素数量。contains(Object o):检查双端队列中是否包含指定的元素。
由于LinkedList实现了List接口,它还提供了List接口的所有方法,如get(int index)、set(int index, E element)、add(int index, E element)等,但这些方法主要用于按索引访问和修改元素,与双端队列的特性关系不大。
综上所述,LinkedList作为Deque的实现,在Java中提供了灵活的双端队列操作,适用于需要频繁从两端添加、删除和访问元素的场景。
ArrayDeque
Java中的ArrayDeque是一个基于数组实现的双端队列,它具有一系列独特的特点、底层结构和常用用法。以下是详细的解析:
特点
- 无容量大小限制:
ArrayDeque的容量是按需增长的,不会受到初始容量的限制。 - 非线程安全:
ArrayDeque不是线程安全的,它没有同步策略,不支持多线程安全访问。因此,在多线程环境下使用时需要外部同步。 - 高效性:
ArrayDeque在作为栈(stack)使用时,性能优于Stack;在作为队列(queue)使用时,性能优于LinkedList。 - 双端操作:
ArrayDeque支持从两端添加和移除元素,提供了addFirst、addLast、removeFirst、removeLast等方法。 - 不能存储null:
ArrayDeque不允许存储null元素,尝试添加null会抛出NullPointerException。 - fail-fast迭代器:
ArrayDeque的迭代器是快速失败(fail-fast)的,但在并发环境下,程序不能依赖这个特性来检测并发修改。
底层结构
ArrayDeque的底层结构是一个动态扩容的数组(Object[]),其中包含了几个关键属性:
- elements:用于存储队列元素的数组,其大小总是2的幂次方,以便于通过位运算进行高效的索引计算。
- head:队列的头部位置索引,表示出队或弹出栈时的元素位置。
- tail:队列的尾部位置索引,表示入队或进栈时的元素位置,且
tail位总是空的,以便于插入新元素。
ArrayDeque通过维护head和tail两个索引来实现双端队列的功能,并通过动态扩容机制来应对元素的增加。
常用用法
ArrayDeque提供了丰富的操作方法,以下是一些常用的方法:
-
添加元素:
addFirst(E e):在双端队列的头部添加元素。addLast(E e):在双端队列的尾部添加元素。offerFirst(E e):在双端队列的头部添加元素,并返回是否添加成功。offerLast(E e):在双端队列的尾部添加元素,并返回是否添加成功。
-
删除元素:
removeFirst():删除并返回双端队列头部的元素,如果队列为空,则抛出NoSuchElementException。removeLast():删除并返回双端队列尾部的元素,如果队列为空,则抛出NoSuchElementException。pollFirst():删除并返回双端队列头部的元素,如果队列为空,则返回null。pollLast():删除并返回双端队列尾部的元素,如果队列为空,则返回null。
-
获取元素:
getFirst():获取但不删除双端队列头部的元素,如果队列为空,则抛出NoSuchElementException。getLast():获取但不删除双端队列尾部的元素,如果队列为空,则抛出NoSuchElementException。
-
检查元素:
peekFirst():获取但不删除双端队列头部的元素,如果队列为空,则返回null。peekLast():获取但不删除双端队列尾部的元素,如果队列为空,则返回null。
ArrayDeque的这些常用方法使得它既可以作为栈使用(只操作头部),也可以作为队列使用(只操作尾部或头部),提供了灵活的数据结构支持。
PriorityQueue
Java中的PriorityQueue是一个非常重要的数据结构,它基于优先级对元素进行排序,而不是基于元素的插入顺序。
特点
-
优先级排序:
PriorityQueue会根据元素的优先级进行排序,而不是按照元素的插入顺序。默认情况下,元素按照其自然顺序(如整数、字符串等)进行排序,但也可以通过提供自定义的Comparator来改变排序规则。 -
无界队列:
PriorityQueue是一个无界队列,即其容量可以动态增长,以满足存储需求。不过,由于它是基于数组实现的,实际上会受到JVM内存的限制。 -
线程不安全:
PriorityQueue不是线程安全的,如果在多线程环境下使用,需要外部同步机制来保证线程安全。对于并发场景,可以考虑使用PriorityBlockingQueue。 -
不允许null元素:
PriorityQueue不允许插入null元素,尝试插入null会抛出NullPointerException。 -
基于堆的实现:
PriorityQueue的底层是通过堆(默认是小根堆)来实现的,这使得其插入和删除操作的时间复杂度均为O(log n),其中n是队列中元素的数量。
底层结构
在Java中,PriorityQueue的底层数据结构是一个动态数组,但它按照堆的性质来组织元素。堆是一种特殊的完全二叉树,其中每个父节点的值都小于或等于(对于小根堆)其子节点的值,或者大于或等于(对于大根堆)其子节点的值。PriorityQueue默认使用小根堆,但可以通过构造函数指定使用大根堆(通过提供自定义的Comparator)。
常用用法
-
插入元素:
- 使用
add(E e)或offer(E e)方法将元素插入到PriorityQueue中。插入后,元素会根据其优先级进行排序。
- 使用
-
删除并获取队首元素:
- 使用
poll()方法删除并返回PriorityQueue中优先级最高的元素(即队首元素)。如果队列为空,则返回null。
- 使用
-
获取队首元素但不删除:
- 使用
peek()方法获取PriorityQueue中优先级最高的元素,但不从队列中删除它。如果队列为空,则返回null。
- 使用
-
检查队列是否为空:
- 使用
isEmpty()方法检查PriorityQueue是否为空。
- 使用
-
获取队列大小:
- 使用
size()方法获取PriorityQueue中元素的数量。
- 使用
-
遍历队列:
- 可以使用
iterator()方法获取PriorityQueue的迭代器,然后通过迭代器遍历队列中的元素。但需要注意的是,由于PriorityQueue是基于优先级的,遍历顺序可能与元素的插入顺序不同。
- 可以使用
-
自定义排序规则:
- 通过在创建
PriorityQueue时提供自定义的Comparator,可以改变元素的排序规则。这样,元素就可以根据自定义的优先级进行排序。
- 通过在创建
综上所述,PriorityQueue是Java中一个非常有用的数据结构,它基于优先级对元素进行排序,并支持动态扩容。通过合理的使用,可以在许多场景下提高程序的效率和性能。
ConcurrentLinkedQueue
Java中的ConcurrentLinkedQueue是一个线程安全的无界队列,它实现了Queue接口,并且采用链表数据结构来存储元素。
特点
-
线程安全:
ConcurrentLinkedQueue使用了一些并发技术(如CAS算法)来保证多线程环境下的安全性,允许多个线程同时对其进行读写操作,无需额外的同步控制。 -
无界队列:
ConcurrentLinkedQueue没有容量限制,可以根据需要动态地添加元素,适用于生产者-消费者模式中的任务队列。 -
非阻塞:由于采用了无锁算法,
ConcurrentLinkedQueue中的操作都是非阻塞的,这在高并发场景下非常有用,因为它可以减少线程之间的等待和竞争。 -
高效性能:
ConcurrentLinkedQueue通过高效的并发算法,如CAS操作,来提高系统的并发能力和性能。这使得它在处理高并发场景下的元素存储和获取时,表现非常出色。 -
内存效率:
ConcurrentLinkedQueue的内存效率很高,因为它只存储当前队列中的元素,而不会存储已经从队列中移除的元素。 -
不支持null元素:需要注意的是,
ConcurrentLinkedQueue不支持存储null元素。
底层结构
ConcurrentLinkedQueue的底层结构是基于链表的无边界队列。它主要包含两个指针(或称为节点引用):head和tail,分别指向链表的头部和尾部。链表的每个节点都是一个内部类Node的实例,Node类中包含两个主要属性:item用于存储节点的值,next用于指向链表中的下一个节点。当进行元素的添加或删除操作时,head和tail指针会相应地更新,以维护队列的先进先出(FIFO)顺序。
常用用法
-
创建队列:使用无参构造函数创建
ConcurrentLinkedQueue对象。ConcurrentLinkedQueue<Integer> queue = new ConcurrentLinkedQueue<>(); -
添加元素:使用
add(E e)或offer(E e)方法将元素添加到队列的尾部。queue.add(1); queue.offer(2); -
移除元素:可以使用
poll()方法从队列的头部移除并返回元素,如果队列为空则返回null。也可以使用remove(Object o)方法移除队列中第一次出现的指定元素(如果存在),成功时返回true,否则返回false。Integer removed = queue.poll(); // 移除并返回头部元素 boolean success = queue.remove(1); // 尝试移除元素1 -
获取元素:使用
peek()方法获取队列头部的元素但不移除它,如果队列为空则返回null。Integer headElement = queue.peek(); -
检查队列状态:使用
isEmpty()方法检查队列是否为空,使用size()方法获取队列中元素的数量(但请注意,由于并发性质,size()方法提供的是一个估计值)。boolean isEmpty = queue.isEmpty(); int size = queue.size();
综上所述,ConcurrentLinkedQueue是一个高效、线程安全、无界的队列实现,适合用于高并发场景下的元素存储和获取。在使用时,需要注意其不支持存储null元素和阻塞操作的特点。
阻塞队列
Array Blocking Queue
Java中的ArrayBlockingQueue是一个基于数组实现的有界阻塞队列,它在多线程环境下提供了线程安全的队列操作。
特点
-
有界性:
ArrayBlockingQueue在创建时需要指定一个固定的大小,之后这个大小就不能再改变了。这意味着队列的容量是有限的,不会无限制地增长,从而避免了内存溢出的问题。 -
阻塞性:当队列满时,如果再有新的元素试图加入队列,那么这个操作会被阻塞,直到队列中有空间可用;同样地,如果队列为空,那么从队列中取元素的操作也会被阻塞,直到队列中有元素可供消费。这种特性使得
ArrayBlockingQueue非常适合作为生产者-消费者模式中的缓冲区。 -
线程安全性:
ArrayBlockingQueue是线程安全的,它通过内部锁机制(如ReentrantLock)保证了在多线程环境下的安全性。因此,在多线程环境中,你可以放心地使用它而不需要担心数据的一致性问题。 -
高效性:由于
ArrayBlockingQueue是基于数组实现的,因此在某些操作(如随机访问)上可能比基于链表的队列(如LinkedBlockingQueue)更高效。但是,由于入队和出队操作共享同一把锁,所以在高并发场景下可能会成为性能瓶颈。
底层结构
ArrayBlockingQueue的底层结构主要包括以下几个部分:
- 数组:用于存储队列元素的数组,一旦初始化,其容量就不可更改。
- 索引:通常包括两个索引,一个用于指向队列头部的元素(takeIndex),另一个用于指向队列尾部的下一个插入位置(putIndex)。这两个索引用于在数组中高效地管理队列的入队和出队操作。
- 锁和条件变量:使用
ReentrantLock作为内部锁来控制对队列的并发访问,并通过Condition(如notEmpty和notFull)来实现阻塞等待和唤醒机制。
常用用法
ArrayBlockingQueue提供了丰富的操作方法来支持队列的入队、出队以及查询等操作。以下是一些常用的方法:
-
构造方法:
ArrayBlockingQueue(int capacity):创建一个具有指定容量(以整数形式指定)的ArrayBlockingQueue。ArrayBlockingQueue(int capacity, boolean fair):创建一个具有指定容量和指定公平性设置的ArrayBlockingQueue。公平性设置为true时,线程将大致按照它们被添加到队列的顺序(FIFO)来获得访问权;设置为false时,则不保证此顺序。
-
入队操作:
boolean add(E e):将指定元素插入此队列的尾部(如果立即可行且不会违反容量限制),返回true;如果此队列已满,则抛出IllegalStateException。boolean offer(E e):将指定元素插入此队列的尾部(如果队列未满),返回true;如果此队列已满,则返回false。void put(E e) throws InterruptedException:将指定元素插入此队列的尾部,如果队列满,则等待可用的空间。
-
出队操作:
E remove():检索并移除此队列的头部,如果此队列为空,则抛出NoSuchElementException。E poll():检索并移除此队列的头部;如果此队列为空,则返回null。E take() throws InterruptedException:检索并移除此队列的头部,在元素变得可用之前,其他线程会在此处阻塞。
-
其他操作:
int size():返回队列中的元素数量。boolean isEmpty():如果队列为空,则返回true。int remainingCapacity():返回队列中剩余可用空间的容量。
通过以上方法,ArrayBlockingQueue在多线程环境中提供了一种高效、安全且易于使用的队列实现方式。
LinkedBlockingQueue
Java中的LinkedBlockingQueue是一个线程安全的阻塞队列,它实现了BlockingQueue接口。
特点
-
线程安全:
LinkedBlockingQueue内部通过锁机制(如ReentrantLock)保证了在多线程环境下的线程安全性,因此可以在多个线程之间共享使用而无需额外的同步控制。 -
阻塞性:当队列满时,尝试向队列中添加元素的线程会被阻塞,直到队列中有空间可用;同样地,当队列为空时,尝试从队列中移除元素的线程也会被阻塞,直到队列中有元素可供移除。
-
可选容量限制:
LinkedBlockingQueue可以在创建时指定容量,如果不指定,则默认容量为Integer.MAX_VALUE,即无界队列。有界队列在达到容量限制时会阻塞插入操作,直到队列中有空间可用。 -
公平性:
LinkedBlockingQueue支持公平性设置(通过构造函数中的fair参数指定),当设置为true时,线程将按照它们被添加到队列的顺序(即FIFO)来获取访问权,这有助于减少饥饿现象。但需要注意的是,公平性通常会降低吞吐量。 -
基于链表实现:
LinkedBlockingQueue内部使用链表结构来存储元素,这使得它在进行插入和删除操作时具有较高的效率,尤其是在队列的头部和尾部进行操作时。
底层结构
LinkedBlockingQueue的底层结构主要包括以下几个部分:
- 链表节点:每个节点都包含元素值、指向前一个节点的引用以及指向下一个节点的引用,从而构成了一个双向链表。
- 锁和条件变量:
LinkedBlockingQueue使用了两把锁(putLock和takeLock)来控制并发访问,分别用于控制插入和删除操作。同时,它还使用了两个条件变量(notFull和notEmpty)来实现线程的阻塞和唤醒。 - 队列头部和尾部:分别通过
head和last指针来指示队列的头部和尾部元素。 - 计数器:使用一个计数器(
count)来记录队列中元素的数量,以便快速判断队列是否为空或已满。
常用用法
LinkedBlockingQueue提供了丰富的操作方法来支持队列的入队、出队以及查询等操作。以下是一些常用的方法:
-
入队操作:
boolean add(E e):向队列尾部添加一个元素,如果队列已满,则抛出IllegalStateException异常。boolean offer(E e):向队列尾部添加一个元素,如果队列已满,则返回false。void put(E e) throws InterruptedException:向队列尾部添加一个元素,如果队列已满,则线程被阻塞直到队列有空间可用。
-
出队操作:
E remove():移除并返回队列头部的元素,如果队列为空,则抛出NoSuchElementException。E poll():移除并返回队列头部的元素,如果队列为空,则返回null。E take() throws InterruptedException:移除并返回队列头部的元素,如果队列为空,则线程被阻塞直到队列有元素可用。
-
其他操作:
int size():返回队列中元素的个数。boolean isEmpty():如果队列为空,则返回true。void clear():清空队列中的所有元素。E peek():返回队列头部的元素,但不移除。
这些操作方法使得LinkedBlockingQueue在多线程环境下能够高效地进行数据的存储和传递,特别适合于实现生产者-消费者模式。
PriorityBlockingQueue
Java中的PriorityBlockingQueue是一个支持优先级的无界阻塞队列,它基于优先级堆(通常是二叉堆)实现,确保了队列中元素的有序性。
特点
-
优先级排序:队列中的元素会根据其自然排序(如果元素实现了
Comparable接口)或者通过构造函数传入的Comparator进行排序。这意味着每次从队列中取出元素时,都是取出当前优先级最高的元素。 -
无界但受系统资源限制:虽然
PriorityBlockingQueue被设计为无界队列,但实际上它的容量受到系统可用内存的限制。当元素不断被添加到队列中,且队列中的元素数量超过系统所能处理的范围时,可能会导致内存溢出。 -
线程安全:作为
BlockingQueue接口的实现,PriorityBlockingQueue提供了线程安全的队列操作,允许多个线程同时访问队列而无需进行外部同步。 -
阻塞特性:虽然
PriorityBlockingQueue是无界的,但在某些场景下(如使用take()或带超时的poll()方法时),如果队列为空,则尝试从队列中取元素的线程会被阻塞,直到队列中有元素可用。 -
灵活性:允许在队列实例化时指定初始容量和比较器,以及在不指定初始容量时使用默认容量(通常为11)。
底层结构
PriorityBlockingQueue的底层结构主要包括以下几个部分:
-
二叉堆:使用二叉堆(通常是最小堆)来存储队列中的元素,确保每次出队的都是优先级最高的元素。二叉堆是一种特殊的完全二叉树,其中每个父节点的值都小于或等于其子节点的值(在最小堆中)。
-
动态扩容:由于
PriorityBlockingQueue是无界的,当元素数量超过当前数组容量时,它会自动进行扩容操作。扩容操作会创建一个新的、更大的数组,并将旧数组中的元素复制到新数组中。为了避免在扩容过程中发生并发问题,扩容操作会先释放锁,然后通过无锁化的CAS(Compare-And-Swap)操作来确保只有一个线程可以成功扩容。 -
锁和条件变量:使用
ReentrantLock作为内部锁来控制对队列的并发访问,并通过Condition(如notEmpty)来实现线程的阻塞和唤醒。
常用用法
PriorityBlockingQueue提供了多种方法来支持队列的入队、出队以及查询等操作。以下是一些常用的方法:
-
入队操作:
boolean add(E e):将指定元素插入此队列,如果此队列已满(实际上对于PriorityBlockingQueue来说,这几乎不可能发生),则抛出IllegalStateException。但请注意,由于PriorityBlockingQueue是无界的,所以这个方法几乎不会抛出异常。boolean offer(E e):将指定元素插入此队列,如果此队列有空间(即未超过系统内存限制),则返回true。void put(E e):将指定元素插入此队列,如果队列满(尽管实际上不太可能),则等待可用的空间。但由于PriorityBlockingQueue是无界的,所以这个方法通常不会阻塞。
-
出队操作:
E remove():检索并移除此队列的头部(即优先级最高的元素),如果此队列为空,则抛出NoSuchElementException。E poll():检索并移除此队列的头部,如果此队列为空,则返回null。E take():检索并移除此队列的头部,在元素变得可用之前,其他线程会在此处阻塞。
-
其他操作:
int size():返回队列中的元素数量。boolean isEmpty():如果队列为空,则返回true。Iterator<E> iterator():返回队列中元素的迭代器,但需要注意的是,迭代器提供的是弱一致性的视图,它反映的是迭代器创建时队列的状态,而不保证反映队列的当前状态。
综上所述,PriorityBlockingQueue是一个功能强大且灵活的优先级队列实现,特别适用于需要按优先级处理任务的场景。
Delay Queue
Java中的DelayQueue是一个支持延时获取元素的阻塞队列,它实现了BlockingQueue接口。
特点
-
延时性:队列中的元素必须实现
Delayed接口,该接口要求元素提供剩余延迟时间(通过getDelay方法)。只有当元素的剩余延迟时间为零或负数时,该元素才能从队列中取出。 -
无界性:
DelayQueue是一个无界队列,这意味着它可以无限期地存储元素,直到系统内存耗尽。 -
有序性:队列中的元素按照它们的延迟时间进行排序,延迟时间最短(即最早到期)的元素会被放在队列的头部。
-
阻塞性:当队列为空时,尝试从队列中取出元素的线程会被阻塞,直到有元素到期。
-
线程安全:
DelayQueue是线程安全的,允许多个线程同时访问队列而无需进行外部同步。
底层结构
DelayQueue的底层结构主要基于PriorityQueue实现,但添加了一些额外的机制来支持延时和阻塞功能。具体来说,它包含以下几个关键部分:
-
PriorityQueue:用于存储队列中的元素,并根据元素的延迟时间进行排序。
PriorityQueue内部实现了一个小顶堆,以确保每次都能快速找到延迟时间最短的元素。 -
ReentrantLock:用于保证队列操作的线程安全。所有对队列的修改操作(如入队、出队)都需要先获取这个锁。
-
Condition:
DelayQueue使用Condition对象(通过ReentrantLock的newCondition方法创建)来实现线程的阻塞和唤醒。当队列为空或没有到期的元素时,尝试从队列中取出元素的线程会在Condition上等待,直到有元素到期并被唤醒。 -
Leader-Follower模式:
DelayQueue采用了一种称为“Leader-Follower”的线程等待模式。当队列中有元素时,第一个调用take()方法的线程会成为leader线程,并在Condition上等待队列头结点剩余的延迟时间。其他线程则成为follower线程,并在Condition上无限期等待。当leader线程苏醒并获取到元素后,它会唤醒一个在Condition上等待的follower线程,该线程可能成为新的leader线程。
常用用法
DelayQueue常用于实现定时任务调度,例如任务调度器中,可以将定时任务封装成Delayed对象放入DelayQueue中,然后由一个线程轮询DelayQueue,当延迟时间到达时执行相应的任务。以下是一些常用的方法:
-
入队操作:
boolean add(E e):将指定元素插入此队列,如果此队列已满(对于DelayQueue来说,这几乎不可能发生),则抛出IllegalStateException。但请注意,由于DelayQueue是无界的,所以这个方法几乎不会抛出异常。boolean offer(E e):将指定元素插入此队列,如果此队列有空间(即未超过系统内存限制),则返回true。void put(E e):将指定元素插入此队列,如果队列满(尽管实际上不太可能),则等待可用的空间。但由于DelayQueue是无界的,所以这个方法通常不会阻塞。
-
出队操作:
E take():检索并移除此队列的头部(即延迟时间最短且已到期的元素),在元素变得可用之前,其他线程会在此处阻塞。E poll():检索并移除此队列的头部(如果已到期),如果此队列为空,则返回null。
-
其他操作:
int size():返回队列中的元素数量。boolean isEmpty():如果队列为空,则返回true。
需要注意的是,由于DelayQueue是无界的,因此在使用时需要特别注意内存管理,避免因为元素过多而导致内存溢出。同时,由于DelayQueue中的元素必须实现Delayed接口,因此在创建元素时需要指定其延迟时间。
Synchronous Queue
Java中的SynchronousQueue是一个特殊的阻塞队列,它的主要特点是内部没有容量,即每个插入操作必须等待另一个线程的删除操作,反之亦然。
特点
-
零容量:
SynchronousQueue的容量为0,这意味着它不能存储任何元素。插入操作和删除操作是紧密耦合的,即生产者线程必须等待消费者线程来取走元素,反之亦然。 -
直接传递:队列中的元素是直接从生产者线程传递给消费者线程的,没有任何中间存储。这种机制适用于需要立即传递数据的情况。
-
支持公平性:
SynchronousQueue提供了两种构造方法,一种是默认的非公平模式,另一种是公平模式。在公平模式下,等待时间最长的线程会优先得到插入或删除元素的机会。 -
线程间协作:
SynchronousQueue提供了一种线程间的协作机制,使得线程之间可以直接进行数据交换,而无需通过中间队列来缓冲。
底层结构
SynchronousQueue的底层实现主要依赖于内部的一个Transferer接口,该接口有两个具体的实现类:TransferStack(基于栈实现,用于非公平模式)和TransferQueue(基于队列实现,用于公平模式)。这些实现类通过维护一系列的内部节点(如请求节点和数据节点)来管理线程的插入和删除操作。
-
TransferStack:基于栈的非公平实现。当线程尝试插入或删除元素时,它会被包装成一个节点并压入栈中。栈顶元素代表当前正在等待的线程。
-
TransferQueue:基于队列的公平实现。与
TransferStack类似,但它在内部维护了一个队列来管理等待的线程,确保等待时间最长的线程能够优先得到服务。
常用用法
SynchronousQueue常用于需要立即进行线程间数据交换的场景,如生产者-消费者模型中的高并发通信。以下是一些常用方法:
-
put(E e):将指定元素插入队列中。如果队列为空(即没有等待的线程来取走元素),则当前线程会被阻塞,直到有另一个线程来取走元素。
-
take():从队列中取出一个元素。如果队列为空(即没有元素可取),则当前线程会被阻塞,直到有另一个线程来插入元素。
-
offer(E e, long timeout, TimeUnit unit):尝试将元素插入队列中,等待指定的时间。如果在等待时间内有线程来取走元素,则插入成功并返回
true;否则,在超时后返回false。 -
poll(long timeout, TimeUnit unit):尝试从队列中取出一个元素,等待指定的时间。如果在等待时间内有元素可取,则返回该元素;否则,在超时后返回
null。
需要注意的是,由于SynchronousQueue的容量为0,因此它不支持如peek、element等查看但不移除元素的操作,这些操作在SynchronousQueue中是没有意义的。
SynchronousQueue在Java并发编程中有广泛的应用,如线程池中的newCachedThreadPool就使用了SynchronousQueue来实现任务的快速调度和处理。此外,它还可以用于实现限流控制、异步任务结果传递等场景。
LinkedTransferQueue
Java中的LinkedTransferQueue是一个基于链表的无界阻塞队列,它实现了BlockingQueue和TransferQueue接口,具有高效、低延迟以及直接的生产者-消费者交互等特点。
特点
-
无界性:
LinkedTransferQueue是一个无界队列,它允许任意数量的元素被添加到队列中,而不用担心队列溢出的问题。 -
高效性:它支持高效的线程间数据传递,特别适合于需要高并发和低延迟的场景。
-
直接传递:
LinkedTransferQueue提供了一种机制,使得生产者可以直接将元素传输给等待的消费者,而不需要将元素存储在队列中。这减少了不必要的上下文切换和内存使用。 -
避免无效通知:在某些其他阻塞队列中,线程可能会由于操作系统或JVM的原因而意外地提前唤醒(虚假唤醒)。
LinkedTransferQueue使用自旋等优化技术来减少这种无效通知,从而提高效率。 -
混合支持非阻塞和阻塞操作:除了基本的插入(offer)、移除(poll)和检查(peek)等操作外,还提供了
transfer、tryTransfer等额外的方法,这些方法允许生产者和消费者之间进行更灵活的交互。
底层结构
LinkedTransferQueue的底层结构是由单链表组成的双重队列结构,类似于SynchronousQueue的公平模式。它使用两个指针(head和tail)来分别指向队列的头部和尾部,并通过CAS(Compare-And-Swap)原子操作来更新这两个指针。每个节点(Node)都包含数据项(item)、后继节点(next)、等待线程(waiter)等信息,并通过volatile关键字保证多线程环境下的可见性。
常用用法
LinkedTransferQueue提供了丰富的方法来支持生产者和消费者之间的交互,以下是一些常用方法:
-
transfer(E e):如果当前存在一个正在等待获取的消费者线程,则立即将元素e传输给该消费者;否则,将元素e插入队列尾部,并等待直到有消费者线程取走该元素。这是一个阻塞操作。
-
tryTransfer(E e):如果当前存在一个正在等待获取的消费者线程,则立即将元素e传输给该消费者;如果不存在,则返回
false,并且不将元素e插入队列。这是一个非阻塞操作。 -
tryTransfer(E e, long timeout, TimeUnit unit):如果当前存在一个正在等待获取的消费者线程,则立即将元素e传输给该消费者;如果不存在,则将元素e插入队列尾部,并等待指定的时间。如果在指定时间内元素e被消费者线程取走,则返回
true;否则,返回false,并且从队列中移除元素e。 -
poll():移除并返回队列头部的元素,如果队列为空,则返回
null。这是一个非阻塞操作。 -
take():移除并返回队列头部的元素,如果队列为空,则阻塞直到有元素可用。
-
peek():返回队列头部的元素,但不移除。如果队列为空,则返回
null。 -
size():由于队列的异步特性,检测当前队列的元素个数需要逐一迭代,可能会得到一个不太准确的结果,尤其是在遍历时有可能队列发生更改。因此,这个方法的使用需要谨慎。
-
hasWaitingConsumer():判断是否存在正在等待获取元素的消费者线程。
LinkedTransferQueue的这些方法使得它特别适合于那些需要高效、低延迟以及直接生产者-消费者交互的并发场景,如工作窃取算法或任务传递系统等。
LinkedBlockingQueue
Java中的LinkedBlockingQueue是一个线程安全的阻塞队列,它实现了BlockingQueue接口,并基于链表结构实现。
特点
- 线程安全:
LinkedBlockingQueue内部使用了锁机制来保证多线程环境下的线程安全,因此可以被多个线程同时使用而不需要额外的同步措施。 - 阻塞特性:当队列满时,插入操作(如
put方法)会被阻塞,直到队列中有空间可用;当队列空时,移除操作(如take方法)会被阻塞,直到队列中有元素可供移除。 - 可选容量限制:
LinkedBlockingQueue可以在初始化时指定最大容量,如果未指定,则默认容量为Integer.MAX_VALUE,即无界队列。 - 公平性:
LinkedBlockingQueue可以选择是否按照FIFO(先进先出)原则对等待在队列中的线程进行调度。如果设置为公平模式(通过设置构造函数中的fair参数为true),则线程会按照请求的顺序被唤醒,但请注意,公平模式可能会降低吞吐量。 - 基于链表:内部使用链表来存储元素,这使得它在插入和删除元素时具有较高的效率。
底层结构
LinkedBlockingQueue的底层结构主要由以下几个部分组成:
- 链表节点(Node):每个节点包含元素数据、前驱节点引用和后继节点引用。
- 头节点(head)和尾节点(last):分别指向队列的首部和尾部,用于快速定位队列的首尾元素。
- 锁机制:通常使用两把锁,一把用于控制插入操作(putLock),另一把用于控制删除操作(takeLock),以提高并发性能。此外,还可能使用条件变量(notEmpty和notFull)来实现线程的阻塞和唤醒。
常用用法
LinkedBlockingQueue提供了多种方法来支持队列的基本操作以及阻塞操作:
- 插入元素:
add(E e):向队列尾部添加一个元素,如果队列已满,则抛出IllegalStateException异常。offer(E e):向队列尾部添加一个元素,如果队列已满,则返回false。put(E e):向队列尾部添加一个元素,如果队列已满,则线程被阻塞直到队列有空间可用。
- 移除元素:
poll():移除并返回队列头部的元素,如果队列为空,则返回null。take():移除并返回队列头部的元素,如果队列为空,则线程被阻塞直到有元素可用。
- 查看元素:
peek():返回队列头部的元素,但不移除。如果队列为空,则返回null。
- 其他操作:
size():返回队列中元素的个数。remainingCapacity():返回队列的剩余容量。isEmpty():判断队列是否为空。clear():清空队列中的所有元素。

浙公网安备 33010602011771号