高清图解28个高并发之数据结构/数据结构场景匹配技巧分析(高并发精通篇二)
Java 集合以 ArrayList、LinkedList、HashSet、TreeSet 和 HashMap 等组件为核心,构筑了强大而灵活的数据结构体系。这些组件精心设计以满足不同的性能和功能需求,如 ArrayList 的动态数组支持快速随机访问,而 LinkedList 的双向链表结构则擅长于频繁的插入和删除操作。HashSet 基于哈希表提供高效的元素查找,TreeSet 则通过红黑树维持元素排序。对于多线程环境,CopyOnWriteArraySet 和 ConcurrentHashMap 等并发集合保证了线程安全,同时优化了读写性能。这些设计精妙的组件,不仅提升了数据处理的效率,也简化了复杂问题的解决方案,了解他的设计就掌握了他的原理,本篇注重设计。
0、本章范围
1、JDK集合数据结构范围
1.3. 队列(Queue)
- LinkedListQueue (作为队列使用时): 支持先进先出(FIFO)。
- ArrayDeque: 双端队列,支持快速插入和删除。
- PriorityQueue: 支持优先级排序的队列。
- ConcurrentLinkedQueue: 线程安全的无界队列。
- BlockingQueue: 支持阻塞操作的队列接口,具体实现包括:
- ArrayBlockingQueue: 有界阻塞队列。
- LinkedBlockingQueue: 基于链表结构的阻塞队列。
- PriorityBlockingQueue: 具有优先级的阻塞队列。
- SynchronousQueue: 不存储元素的阻塞队列,主要用于任务窃取。
选择范围
队列类型 | 支持的操作 | 线程安全 | 阻塞操作 | 排序 | 上限 | 适用场景 |
---|---|---|---|---|---|---|
LinkedList (作为队列) | FIFO | 否 | 否 | 否 | 无 | 单线程环境,频繁插入/删除操作 |
ArrayDeque | 双端队列 | 否 | 否 | 否 | 无 | 需要快速插入和删除的双端队列操作 |
PriorityQueue | 优先级队列 | 否 | 否 | 是 | 无 | 需要根据优先级处理元素的场景 |
ConcurrentLinkedQueue | FIFO | 是 | 否 | 否 | 无 | 多线程环境,高并发的无界队列操作 |
ArrayBlockingQueue | FIFO | 是 | 是 | 否 | 有 | 多线程环境,需要阻塞操作的有界队列 |
LinkedBlockingQueue | FIFO | 是 | 是 | 否 | 无 | 多线程环境,需要阻塞操作的无界队列 |
PriorityBlockingQueue | 优先级队列 | 是 | 是 | 是 | 无 | 多线程环境,需要优先级排序的阻塞队列 |
SynchronousQueue | 非阻塞FIFO | 是 | 是 | 否 | 无 | 任务窃取、线程间单个元素传递 |
1.4. 双端队列(Deque)
- ArrayDeque: 基于动态数组,实现双向队列。
- LinkedListDeque (作为双端队列使用时): 基于链表,实现双向队列。
选择范围
特性 | ArrayDeque | LinkedList |
---|---|---|
基础结构 | 动态数组 | 双向链表 |
插入和删除性能 | 两端插入和删除都是 O(1) | 两端插入和删除都是 O(1),但涉及节点连接 |
内存占用 | 相对较高 | 相对较低 |
扩容操作 | 需要扩容,扩容时有临时性能损耗 | 无需扩容 |
随机访问性能 | 支持快速随机访问 | 不支持快速随机访问 |
线程安全 | 否 | 否 |
适用场景 | - 需要快速插入和删除 - 需要频繁访问中间元素 - 单线程或锁控制的多线程环境 |
- 需要插入和删除元素 - 元素经常在队列中移动 - 多线程环境且元素插入位置不定 |
2、集合数据结构设计与分析
2.1 LinkedListQueue(业务结构)
LinkedListQueue 是一个基于链表实现的队列,它通常由一个双向链表构成,支持在队列的前端(头部)进行出队操作,以及在队列的后端(尾部)进行入队操作。
设计思考:
- 需求场景:
- 在许多编程任务中,需要一个能够快速进行插入和删除操作的队列结构。例如,在实现缓冲区管理、任务调度、消息队列等应用中,快速的入队和出队操作是非常重要的。
- 现有技术局限性:
- 传统的数组类型在 Java 中是固定长度的,一旦创建,其大小不能改变,这限制了其在需要动态大小管理时的使用。
Vector
类似于ArrayList
,但它是线程安全的,使用synchronized
进行同步,导致并发性能较差。ArrayDeque
提供了双端队列功能,但在某些情况下,可能需要进行数组的扩容或缩容操作。
- 技术融合:
LinkedListQueue
结合了LinkedList
的高效插入和删除特性,并提供了队列所需的先进先出(FIFO)的数据管理方式。它通过链表的双向链接特性,允许从两端快速地添加或移除元素,特别适合实现队列和双端队列。
- 设计理念:
LinkedListQueue
通过使用链表结构,可以有效地进行插入和删除操作,因为这些操作仅需要改变节点的指针,而不需要移动整个数组。它实现了Queue
接口,提供了队列所需的所有操作,包括offer
、poll
、peek
等。
- 实现方式:
LinkedListQueue
由一系列Node
对象组成,每个Node
包含数据和两个引用(previous
和next
),分别指向前一个和后一个节点。它提供了enqueue
(入队)和dequeue
(出队)操作,以及查看队列头部和尾部元素的方法,而不需要遍历整个链表。
2.1.1 数据结构
图说明:
- LinkedListQueue:
- 表示 LinkedListQueue 类的实例,是一个基于链表的队列数据结构。
- head:
- 表示队列的头节点,它指向队列中的第一个元素,用于出队操作。
- tail:
- 表示队列的尾节点,它指向队列中的最后一个元素,用于入队操作。
- Node:
- 链表中的节点,包含数据和两个引用,分别指向前一个节点和后一个节点。
- data:
- 节点中存储的数据。
- prev:
- 节点的前驱引用,指向前一个节点。
- next:
- 节点的后继引用,指向后一个节点。
2.1.2 执行流程
图说明:
- 开始:执行操作的起始点。
- 创建 LinkedListQueue 实例:初始化
LinkedListQueue
对象。 - 入队操作:执行将元素添加到队列尾部的操作。
- 添加元素到队尾:在队列的尾部添加新元素。
- 结束入队:完成入队操作。
- 出队操作:执行将元素从队列头部移除的操作。
- 移除队首元素:从队列头部移除元素。
- 结束出队:完成出队操作。
- 遍历队列:从头到尾遍历队列中的所有元素。
- 从头节点开始:从队列的头节点开始遍历。
- 访问每个节点:顺序访问链表中的每个节点。
- 读取节点数据:读取每个节点中存储的数据。
- 结束遍历:完成队列遍历。
- 结束:所有操作完成。
2.2 ArrayDeque(业务结构)
ArrayDeque 是 Java 中的一个双端队列(Deque)实现,它基于动态数组来实现。
设计思考:
- 需求场景:
- 在许多编程任务中,需要一个支持快速插入和删除操作的队列,特别是在队列两端进行操作时。例如,在实现栈、队列、双向队列等数据结构时,这些操作非常常见。
- 现有技术局限性:
ArrayList
提供了快速的随机访问能力,但在进行插入和删除操作时,特别是在队列的两端,可能需要移动数组中的大量元素,导致效率低下。LinkedList
虽然提供了高效的插入和删除操作,但其随机访问性能较差,且内存开销较大。
- 技术融合:
ArrayDeque
结合了数组的快速随机访问特性和链表的高效插入删除特性,使用一个可变大小的数组来存储元素,并提供了在数组两端快速添加或移除元素的能力。
- 设计理念:
ArrayDeque
旨在提供一个高效的双端队列实现,它允许元素从两端快速地被插入或删除,同时保持了较高的内存效率和访问性能。
- 实现方式:
ArrayDeque
内部使用一个可变大小的数组来存储元素,数组中的元素以循环方式存储,这样可以在数组的两端以 O(1) 的时间复杂度进行插入和删除操作。
内部细节:
- 数组扩容:
- 当元素添加到
ArrayDeque
时,如果当前数组没有足够的空间来存储新元素,ArrayDeque
会创建一个新的、更大的数组,并将旧数组中的所有元素复制到新数组中。 - 新数组的大小通常是原始数组大小的两倍,但也可能根据当前的元素数量和负载因子进行调整。
- 当元素添加到
- 头部和尾部操作:
ArrayDeque
维护两个索引,分别表示头部和尾部的位置。- 当元素被添加到尾部时,尾部索引会增加;当元素被从头部移除时,头部索引会增加。
- 当头部或尾部索引达到数组末尾时,它们会回绕到数组的开始位置,因为
ArrayDeque
的数组是循环使用的。
- 数组收缩:
- 虽然
ArrayDeque
主要通过扩容来适应元素的增加,但它并不会自动收缩数组的大小。如果需要减少内存占用,可以手动创建一个新的、更小的数组,并将当前的元素复制到新数组中。
- 虽然
- 随机访问:
- 尽管
ArrayDeque
提供了在两端快速插入和删除的能力,但它也支持快速的随机访问,因为底层数据结构是数组。
- 尽管
- 插入元素:
- 在新数组中,头部索引前的位置现在有了空间,
ArrayDeque
可以直接在头部索引前插入新元素。由于数组是循环使用的,如果头部索引是 0,新元素将被插入到数组的最后一个位置,并且头部索引将更新为指向新元素。
- 在新数组中,头部索引前的位置现在有了空间,
- 更新头部索引:
- 插入元素后,头部索引会相应地向前移动,指向新插入的元素。
2.2.1 数据结构
图说明:
- ArrayDeque:
- 表示
ArrayDeque
类的实例,是一个基于数组实现的双端队列。
- 表示
- 数组 (Elements) :
ArrayDeque
内部使用一个动态数组来存储队列中的元素。
- Head Index:
- 表示队列头部的索引,用于
poll
或peek
操作。
- 表示队列头部的索引,用于
- Tail Index:
- 表示队列尾部的索引,用于
offer
或peekLast
操作。
- 表示队列尾部的索引,用于
- Array Elements:
- 数组中的元素,存储队列中的数据。
- Element 1, Element 2, Element n:
- 表示数组中的具体元素。
2.2.2 执行流程
图说明:
- 开始:执行操作的起始点。
- 创建 ArrayDeque 实例:初始化
ArrayDeque
对象。 - 入队操作:执行将元素添加到队列尾部的操作。
- 计算尾索引:计算当前尾元素的索引位置。
- 添加元素到尾:在尾索引位置添加新元素。
- 更新尾索引:更新尾索引以指向下一个可用位置。
- 结束入队:完成入队操作。
- 出队操作:执行将元素从队列头部移除的操作。
- 计算头索引:计算当前头元素的索引位置。
- 移除元素从头:移除头索引位置的元素。
- 更新头索引:更新头索引以指向下一个可用位置。
- 结束出队:完成出队操作。
- 头部查看:查看队列头部的元素。
- 读取头元素:读取头索引位置的元素。
- 尾部查看:查看队列尾部的元素。
- 读取尾元素:读取尾索引前一个位置的元素。
- 结束查看:完成元素查看操作。
- 结束:所有操作完成。
2.3 PriorityQueue(业务结构)
PriorityQueue 是 Java 中的一个优先队列实现,通常基于堆数据结构(默认为二叉堆,也可以通过构造函数指定其他类型的堆,如斐波那契堆),PriorityQueue 维护了一个元素的集合,这些元素按照自然顺序或者通过提供的 Comparator 进行排序。
设计思考:
- 需求场景:
- 在许多编程任务中,需要根据元素的优先级来处理数据。例如,在任务调度、事件驱动模拟、资源分配等问题中,某些元素需要比其他元素更优先地被处理。
- 现有技术局限性:
- 标准的队列(如
LinkedList
实现的队列)仅按照元素的到达顺序进行处理,不支持基于优先级的排序。 - 手动维护一个排序的列表来进行优先级处理会增加额外的复杂性和性能开销,尤其是在插入和删除操作频繁的场景中。
- 标准的队列(如
- 技术融合:
- PriorityQueue 结合了队列的先进先出(FIFO)特性和优先级排序的需要,使用一个优先级规则来决定元素的处理顺序。
- 设计理念:
- PriorityQueue 允许元素根据自然顺序或者通过提供的
Comparator
来决定优先级,确保每次取出的元素都是优先级最高的。
- PriorityQueue 允许元素根据自然顺序或者通过提供的
- 实现方式:
- PriorityQueue 通常基于堆数据结构(通常是二叉堆)实现,其中元素按照优先级顺序组织。在插入新元素时,堆会重新调整以维持优先级顺序。
2.3.1 数据结构
图说明:
- PriorityQueue:
- 表示
PriorityQueue
类的实例,是一个基于堆的优先队列。
- 表示
- Heap:
- 表示优先队列底层的堆结构,通常是一个二叉堆。
- Array:
- 堆通常使用数组来存储元素,以实现快速的访问和调整操作。
- 元素1, 元素2, 元素n:
- 表示数组中存储的具体元素,它们根据优先级顺序被组织在堆中。
- Comparator:
- 优先队列可以使用一个
Comparator
来确定元素的优先级顺序。
- 优先队列可以使用一个
2.3.2 执行流程
图说明:
- 开始:执行操作的起始点。
- 创建 PriorityQueue 实例:初始化
PriorityQueue
对象。 - 插入元素(offer) :执行将元素添加到优先队列的操作。
- 构建堆:新元素被添加到堆中,可能需要构建一个新的堆结构。
- 调整堆:根据优先级调整堆,确保父节点的优先级高于子节点(或反之,取决于是否是最小堆或最大堆)。
- 结束插入:完成元素的插入操作。
- 删除元素(poll) :执行将优先级最高的元素从优先队列中移除的操作。
- 移除顶部元素:移除堆顶部的元素,即优先级最高的元素。
- 重新调整堆:移除顶部元素后,重新调整堆结构以维持优先级顺序。
- 结束删除:完成元素的删除操作。
- 查找顶部元素(peek) :查看但不移除优先队列中优先级最高的元素。
- 读取顶部元素:读取堆顶部的元素,即优先级最高的元素。
- 结束查找:完成查找操作。
- 结束:所有操作完成。
2.4 ConcurrentLinkedQueue
ConcurrentLinkedQueue 是 Java 中一个线程安全的无界队列,基于链接节点的非阻塞算法实现。
设计思考:
- 需求场景:
- 在多线程编程中,需要一种线程安全的队列来处理任务或数据传递,其中元素的插入和删除操作需要高效且不会导致线程阻塞。
- 适用于负载均衡、任务分配、并发数据处理等场景,其中多个生产者线程向队列中添加元素,多个消费者线程从队列中移除元素。
- 现有技术局限性:
- 传统的
Queue
实现,如LinkedList
或ArrayList
,虽然提供了队列操作,但不是线程安全的,需要额外的同步措施来保证线程安全,这会降低并发性能。 Vector
和Collections.synchronizedQueue()
提供了线程安全,但使用synchronized
方法或块会导致线程阻塞,不适合高并发环境。
- 传统的
- 技术融合:
- ConcurrentLinkedQueue 结合了链表的高效插入和删除特性和无锁(lock-free)的并发控制机制,以提高在高并发环境下的性能。
- 设计理念:
- ConcurrentLinkedQueue 旨在提供一个高性能的线程安全队列,它利用现代多核处理器的缓存一致性和原子操作来实现高效的并发控制,避免了传统的锁机制。
- 实现方式:
- ConcurrentLinkedQueue 通常基于链接节点的链表数据结构实现,其中每个节点包含一个指向下一个节点的引用。
- 它使用 CAS(Compare-And-Swap)操作来实现无锁的插入和删除,确保在多线程环境下的线程安全。
2.4.1 数据结构
图说明:
- ConcurrentLinkedQueue:
- 表示
ConcurrentLinkedQueue
类的实例,是一个线程安全的无界队列。
- 表示
- Pointer Head:
- 队列的头指针,指向队列中的第一个有效节点。
- Pointer Tail:
- 队列的尾指针,指向队列中最后一个节点的前一个节点,这样可以快速地在
Node2
后面添加新的节点。
- 队列的尾指针,指向队列中最后一个节点的前一个节点,这样可以快速地在
- Node1 & Node2:
- 表示队列中的两个具体节点,每个节点都包含数据和指向下一个节点的引用。
- Node Next:
- 表示
Node2
指向下一个节点的引用,如果Node2
后面还有节点,那么Node2
的next
将指向该节点。
- 表示
- Element Data1 & Data2:
- 表示两个节点中存储的数据。
2.4.2 执行流程
图说明:
- 创建 ConcurrentLinkedQueue 实例:初始化
ConcurrentLinkedQueue
对象。 - 入队操作(offer) :执行将元素添加到队列尾部的操作。
- 构造新节点:创建一个新的节点来存储入队的元素。
- 查找尾节点:找到当前队列的尾节点。
- 尾节点入队:将新节点添加到尾节点之后。
- 更新尾指针:更新尾指针以指向新的尾节点。
- 结束入队:完成入队操作。
- 出队操作(poll) :执行将元素从队列头部移除的操作。
- 查找头节点:找到当前队列的头节点。
- 判断队列是否为空:检查队列是否为空,如果为空则直接返回。
- 移除头节点:从队列中移除头节点。
- 更新头指针:更新头指针以指向新的头节点。
- 返回被移除元素:返回被移除节点的元素。
- 结束出队:完成出队操作。
- 查看队首元素(peek) :查看队列头部的元素。
- 查找头节点:找到当前队列的头节点。
- 返回头节点元素:返回头节点的元素,但不移除它。
- 结束查看:完成查看操作。
2.5 ArrayBlockingQueue
ArrayBlockingQueue
是 Java 中的一个线程安全的有界阻塞队列,通常基于一个固定大小的数组实现。
设计思考:
- 需求场景:
- 在多线程编程中,需要一个线程安全的队列来处理任务或数据传递,其中元素的插入和删除操作需要在多线程环境下同步进行。
- 适用于任务调度、线程间通信、生产者-消费者问题等场景,其中队列的大小需要被限制以避免无限制的资源使用。
- 现有技术局限性:
- 传统的
Queue
实现,如LinkedList
,虽然提供了队列操作,但不是线程安全的,需要额外的同步措施来保证线程安全。 - 同步措施(如
synchronized
)可能导致线程阻塞,影响并发性能,特别是在高竞争环境下。
- 传统的
- 技术融合:
- ArrayBlockingQueue 结合了数组的快速随机访问特性和阻塞队列机制,使用一个固定大小的数组来存储元素,并提供了阻塞插入和删除操作。
- 设计理念:
- ArrayBlockingQueue 旨在提供一个线程安全的队列,它在队列满时阻塞插入操作,在队列空时阻塞删除操作,从而保证了线程间有效的协调和资源管理。
- 实现方式:
- ArrayBlockingQueue 内部使用一个固定大小的数组来存储元素,同时使用两个指针(头部和尾部)来指示队列的当前状态。
- 它使用了
ReentrantLock
和条件变量(notEmpty
和notFull
)来实现阻塞和唤醒机制,确保在多线程环境下的线程安全和性能。
2.5.1 数据结构
图说明:
- ArrayBlockingQueue:
- 表示
ArrayBlockingQueue
类的实例,是一个线程安全的有界阻塞队列。
- 表示
- Array:
ArrayBlockingQueue
内部使用一个固定大小的数组来存储队列中的元素。
- Head Pointer:
- 头指针指向队列中第一个元素的位置,用于出队操作。
- Tail Pointer:
- 尾指针指向队列中下一个插入元素的位置,用于入队操作。
- Elements:
- 数组中的元素,按照先进先出的顺序排列。
- Element 1, Element 2, Element n:
- 表示数组中的具体元素,它们按照队列的顺序存储。
2.5.2 执行流程
图说明:
- 创建 ArrayBlockingQueue 实例:初始化
ArrayBlockingQueue
对象。 - 入队操作(offer/put) :执行将元素添加到队列尾部的操作。
- 检查队列是否已满:在添加元素前检查队列是否已达到最大容量。
- 将元素添加到队尾:如果队列未满,将新元素添加到队尾。
- 更新尾指针:添加元素后,更新尾指针位置。
- 结束入队:完成入队操作。
- 出队操作(poll/take) :执行将元素从队列头部移除的操作。
- 检查队列是否为空:在移除元素前检查队列是否为空。
- 将元素从头部移除:如果队列不为空,将头部元素移除。
- 更新头指针:移除元素后,更新头指针位置。
- 返回被移除的元素:返回被出队的元素。
- 结束出队:完成出队操作。
- 查看队首元素(peek) :查看队列头部的元素而不移除它。
- 读取队列头部元素:读取但不移除队列头部的元素。
- 结束查看:完成查看操作。
2.5.3 锁流程
图说明:
- 创建 ArrayBlockingQueue 实例:初始化
ArrayBlockingQueue
对象。 - 生产者线程尝试入队(offer/put) :生产者线程尝试将元素添加到队列中。
- 队列满检查:检查队列是否已满。
- 队列有空间:如果队列未满,生产者线程可以继续入队操作。
- 元素入队:将元素添加到队列尾部。
- 更新尾指针:更新尾指针以指向新的队尾位置。
- 消费者线程尝试出队(poll/take) :消费者线程尝试从队列中移除元素。
- 队列空检查:检查队列是否为空。
- 队列有元素:如果队列不为空,消费者线程可以继续出队操作。
- 元素出队:从队列头部移除元素。
- 更新头指针:更新头指针以指向新的队首位置。
- 返回出队元素:返回被移除的元素。
- 生产者线程等待:如果队列已满,生产者线程将被阻塞并等待。
- 消费者线程等待:如果队列为空,消费者线程将被阻塞并等待。
- 被唤醒继续操作:当队列中有空间或元素时,等待的线程被唤醒并继续操作。
2.6 LinkedBlockingQueue
LinkedBlockingQueue
是 Java 中的一个线程安全的阻塞队列,通常基于链表实现。
设计思考:
- 需求场景:
- 在多线程编程中,需要一个高效的线程安全队列来处理生产者-消费者问题,其中元素的插入和删除操作需要在多线程环境下同步进行,且不会耗尽内存资源。
- 适用于任务调度、线程间通信、负载均衡等场景,其中队列的大小可能需要动态调整,以适应不同的负载条件。
- 现有技术局限性
- 传统的
Queue
实现,如ArrayList
或LinkedList
,虽然提供了队列操作,但不是线程安全的,需要额外的同步措施来保证线程安全。 ArrayBlockingQueue
提供了阻塞队列功能,但大小固定,不适合需要动态调整队列大小的场景。
- 传统的
- 技术融合:
- LinkedBlockingQueue 结合了链表的动态大小特性和阻塞队列机制,使用一个链表来存储元素,并提供了阻塞插入和删除操作。
- 设计理念:
- LinkedBlockingQueue 旨在提供一个线程安全的队列,它在队列满时阻塞插入操作,在队列空时阻塞删除操作,同时支持可选的有界或无界队列,以适应不同的应用需求。
- 实现方式:
- LinkedBlockingQueue 内部使用一个链表来存储元素,同时使用
ReentrantLock
和条件变量(notEmpty
和notFull
)来实现阻塞和唤醒机制,确保在多线程环境下的线程安全和性能。 - 它提供了一个可选的容量限制,如果未指定容量,则队列可以无限增长,但如果指定了容量,则在队列满时会阻塞插入操作。
- LinkedBlockingQueue 内部使用一个链表来存储元素,同时使用
2.6.1 数据结构
图说明:
- LinkedBlockingQueue:
- 表示
LinkedBlockingQueue
类的实例,是一个线程安全的阻塞队列。
- 表示
- Linked List:
LinkedBlockingQueue
内部使用一个链表来存储队列中的元素。
- Node:
- 链表中的节点,每个节点包含数据和两个引用,分别指向前一个节点和后一个节点。
- Head Pointer:
- 头指针指向队列中第一个元素的节点,用于出队操作。
- Tail Pointer:
- 尾指针指向队列中最后一个元素的节点,用于入队操作。
- Node next:
- 节点的
next
引用指向链表中的下一个节点。
- 节点的
- Element Data:
- 节点存储的数据。
- Node prev:
- 节点的
prev
引用指向链表中的前一个节点。
- 节点的
2.6.2 执行流程
图说明:
- 创建 LinkedBlockingQueue 实例:初始化
LinkedBlockingQueue
对象。 - 入队操作(put/offer) :执行将元素添加到队列尾部的操作。
- 检查队列是否已满:在添加元素前检查队列是否已达到最大容量。
- 队列未满:如果队列未满,可以继续入队操作。
- 将元素添加到队尾:将新元素添加到队列尾部。
- 更新尾指针:更新尾指针以指向新的队尾位置。
- 唤醒等待的消费者:如果有线程在等待队列非空,唤醒它们。
- 结束入队:完成入队操作。
- 出队操作(take/poll) :执行将元素从队列头部移除的操作。
- 检查队列是否为空:在移除元素前检查队列是否为空。
- 队列不为空:如果队列不为空,可以继续出队操作。
- 将元素从头部移除:从队列头部移除元素。
- 更新头指针:更新头指针以指向新的队首位置。
- 返回被移除的元素:返回被出队的元素。
- 唤醒等待的生产者:如果有线程在等待队列非满,唤醒它们。
- 结束出队:完成出队操作。
- 查看队首元素(peek) :查看队列头部的元素而不移除它。
- 读取队列头部元素:读取但不移除队列头部的元素。
- 结束查看:完成查看操作。
2.5.3 锁流程
图说明:
- 创建 LinkedBlockingQueue 实例:初始化
LinkedBlockingQueue
对象,它内部包含一个锁(Lock)和两个条件变量(notEmpty 和 notFull)。 - 入队操作(put/offer) :生产者线程尝试将元素添加到队列中。
- 获取锁:在修改队列前,先获取锁以确保线程安全。
- 检查队列是否已满:检查队列是否已达到最大容量。
- 队列未满:如果队列未满,可以继续入队操作。
- 将元素添加到队尾:将新元素添加到队列尾部。
- 释放锁:完成入队操作后,释放锁以便其他线程可以访问队列。
- 通知等待的消费者:如果有消费者线程在等待队列非空,通知它们现在可以进行出队操作。
- 出队操作(take/poll) :消费者线程尝试从队列中移除元素。
- 获取锁:在修改队列前,先获取锁以确保线程安全。
- 检查队列是否为空:检查队列是否为空。
- 队列不为空:如果队列不为空,可以继续出队操作。
- 将元素从头部移除:从队列头部移除元素。
- 释放锁:完成出队操作后,释放锁以便其他线程可以访问队列。
- 通知等待的生产者:如果有生产者线程在等待队列非满,通知它们现在可以进行入队操作。
- 查看队首元素(peek) :查看队列头部的元素而不移除它。
- 获取锁:在读取队列头部元素前,先获取锁以确保线程安全。
- 读取队列头部元素:读取但不移除队列头部的元素。
- 释放锁:完成读取操作后,释放锁以便其他线程可以访问队列。
- 结束查看:完成查看操作。
2.7 PriorityBlockingQueue
PriorityBlockingQueue
是 Java 中的一个线程安全的阻塞队列,通常基于优先队列实现。
设计思考:
- 需求场景:
- 在多线程环境中,需要根据元素的优先级来处理任务或数据,同时保证线程安全。例如,在任务调度系统中,高优先级的任务需要优先执行。
- 适用于需要优先级处理的场景,如实时系统、事件驱动系统、资源分配等,其中某些操作必须优先于其他操作执行。
- 现有技术局限性:
PriorityQueue
提供了优先队列的功能,但它不是线程安全的,需要额外的同步措施来保证线程安全,这可能会影响性能。- 传统的阻塞队列实现,如
ArrayBlockingQueue
和LinkedBlockingQueue
,不直接支持优先级排序。
- 技术融合:
- PriorityBlockingQueue 结合了优先队列的优先级排序特性和阻塞队列的线程安全性,使用一个优先级规则来决定元素的处理顺序,并提供了阻塞插入和删除操作。
- 设计理念:
- PriorityBlockingQueue 旨在提供一个线程安全的优先队列,它允许元素根据自然顺序或者通过提供的
Comparator
来决定优先级,同时确保在多线程环境下的线程安全。
- PriorityBlockingQueue 旨在提供一个线程安全的优先队列,它允许元素根据自然顺序或者通过提供的
- 实现方式:
- PriorityBlockingQueue 通常基于堆数据结构实现,其中元素按照优先级顺序组织。在插入新元素时,堆会重新调整以维持优先级顺序。
- 它使用了
ReentrantLock
和条件变量来实现阻塞和唤醒机制,确保在多线程环境下的线程安全和性能。
2.7.1 数据结构
图说明:
- PriorityBlockingQueue:
- 表示
PriorityBlockingQueue
类的实例,是一个线程安全的阻塞队列。
- 表示
- Array:
PriorityBlockingQueue
内部使用一个数组来存储队列中的元素。
- Head Index:
- 头索引指向队列中第一个元素的位置,用于出队操作。
- Tail Index:
- 尾索引指向队列中下一个插入元素的位置,用于入队操作。
- Available Slot:
- 可用槽位表示数组中的空闲位置,可用于插入新元素。
- Element Data:
- 元素数据表示存储在数组中的队列元素。
- Lock:
PriorityBlockingQueue
使用一个锁(ReentrantLock
)来确保线程安全。
- Not Empty Condition:
- 非空条件变量用于阻塞那些在队列为空时尝试进行出队操作的消费者线程。
- Not Full Condition:
- 非满条件变量用于阻塞那些在队列满时尝试进行入队操作的生产者线程。
- Producer Thread:
- 生产者线程表示尝试向队列中添加元素的线程。
- Consumer Thread:
- 消费者线程表示尝试从队列中移除元素的线程。
2.7.2 执行流程
图说明:
- 创建 PriorityBlockingQueue 实例:初始化
PriorityBlockingQueue
对象,它内部包含一个锁(Lock)和两个条件变量(notEmpty 和 notFull)。 - 生产者线程尝试入队(put/offer) :生产者线程尝试将元素添加到队列中。
- 获取锁:在修改队列前,先获取锁以确保线程安全。
- 检查队列是否已满:检查队列是否已达到最大容量。
- 队列未满:如果队列未满,可以继续入队操作。
- 将元素添加到队尾:将新元素添加到队列尾部。
- 释放锁:完成入队操作后,释放锁以便其他线程可以访问队列。
- 通知等待的消费者:如果有消费者线程在等待队列非空,通知它们现在可以进行出队操作。
- 消费者线程尝试出队(poll/take) :消费者线程尝试从队列中移除元素。
- 获取锁:在修改队列前,先获取锁以确保线程安全。
- 检查队列是否为空:检查队列是否为空。
- 队列不为空:如果队列不为空,可以继续出队操作。
- 将元素从头部移除:从队列头部移除元素。
- 释放锁:完成出队操作后,释放锁以便其他线程可以访问队列。
- 通知等待的生产者:如果有生产者线程在等待队列非满,通知它们现在可以进行入队操作。
- 查看队首元素(peek) :查看队列头部的元素而不移除它。
- 获取锁:在读取队列头部元素前,先获取锁以确保线程安全。
- 读取队列头部元素:读取但不移除队列头部的元素。
- 释放锁:完成读取操作后,释放锁以便其他线程可以访问队列。
2.8 SynchronousQueue
SynchronousQueue
是 Java 中一个非常特殊的线程安全队列,它实际上不存储任何元素,而是直接将生产者线程和消费者线程进行匹配。
设计思考:
- 需求场景:
- 在某些高并发处理场景中,如无缓冲的消息传递、实时任务处理等,需要一种机制使得生产者线程直接将任务传递给消费者线程,而不需要任何中间缓冲。
- 适用于线程间需要紧密协作的场景,其中生产者生成任务后,消费者应立即处理这些任务,无需等待或延迟。
- 现有技术局限性:
- 传统的队列实现,如
ArrayBlockingQueue
、LinkedBlockingQueue
和PriorityBlockingQueue
,都使用内部缓冲区来存储元素,这可能会导致生产者线程在缓冲区满时阻塞,或者消费者线程在缓冲区空时阻塞。 - 这些队列实现在处理需要即时任务传递的场景时,由于缓冲区的存在,无法实现生产者和消费者之间的直接交互。
- 传统的队列实现,如
- 技术融合:
- SynchronousQueue 结合了阻塞队列的线程同步特性和无缓冲直接传递的需求,它不存储任何元素,而是将每个插入操作与一个移除操作匹配起来,实现了生产者和消费者之间的直接任务传递。
- 设计理念:
- SynchronousQueue 设计为一个无内部容量的队列,它依赖于消费者线程的可用性来插入元素,实现了生产者和消费者之间的同步。
- 这种设计允许任务在生产者和消费者之间直接传递,无需任何中间缓冲,从而减少了延迟并提高了处理效率。
- 实现方式:
- SynchronousQueue 内部使用一组等待队列来管理等待插入和移除操作的线程,当一个元素被插入时,它会被立即传递给一个等待的消费者线程,反之亦然。
- 它使用了
ReentrantLock
和条件变量来控制线程的等待和唤醒,确保了线程间的同步和协调。
2.8.1 数据结构
图说明:
- 同步队列 SynchronousQueue:表示
SynchronousQueue
类的实例,是一个线程安全的阻塞队列,不存储元素,直接匹配生产者和消费者线程。 - 虚拟容器 Virtual Container:表示
SynchronousQueue
内部的虚拟容器,用于协调生产者和消费者线程。这个容器实际上并不存储元素,而是作为一个协调点。 - 空队列状态 Empty:表示虚拟容器的初始状态,为空。
- 非空队列状态 NotEmpty:表示虚拟容器在生产者线程添加元素后的状态,不为空。
- 入队线程 Offer Thread:表示生产者线程尝试向虚拟容器添加元素。
- 出队线程 Poll Thread:表示消费者线程尝试从虚拟容器移除元素。
- 锁 Lock:用于控制对虚拟容器访问的锁。
- 等待队列 Wait Queue:存储等待的线程。
- 入队操作 Offer Operation:生产者线程执行的操作,尝试将元素放入虚拟容器。
- 出队操作 Poll Operation:消费者线程执行的操作,尝试从虚拟容器取出元素。
- 线程等待 Thread Waits:如果虚拟容器为空,入队线程将在此等待。
- 线程通知 Thread Signals:当出队线程执行出队操作后,入队线程被通知。
2.8.2 执行流程
图说明:
- 生产者线程到达:表示生产者线程准备向
SynchronousQueue
进行入队操作。 - 消费者线程到达:表示消费者线程准备从
SynchronousQueue
进行出队操作。 - 队列为空:检查
SynchronousQueue
是否为空。 - 队列不为空:检查
SynchronousQueue
是否有元素可供出队。 - 生产者等待:如果队列为空,生产者线程将等待消费者线程的出队操作。
- 消费者等待:如果队列为空,消费者线程将等待生产者线程的入队操作。
- 生产者唤醒消费者:生产者线程完成入队操作后,唤醒等待的消费者线程。
- 消费者唤醒生产者:消费者线程完成出队操作后,唤醒等待的生产者线程。
- 生产者入队操作:生产者线程执行入队操作,将元素添加到队列。
- 消费者出队操作:消费者线程执行出队操作,从队列中移除元素。
2.8.3 应用案例
import java.util.concurrent.SynchronousQueue;
public class SynchronousQueueExample {
public static void main(String[] args) {
SynchronousQueue<Integer> queue = new SynchronousQueue<>();
// 生产者线程
Thread producer = new Thread(() -> {
try {
Thread.sleep(1000); // 模拟生产耗时
int item = 1; // 生产一个产品
System.out.println("Producer offering item: " + item);
queue.put(item); // 将产品放入队列
System.out.println("Producer offered item: " + item);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
// 消费者线程
Thread consumer = new Thread(() -> {
try {
Integer item = queue.take(); // 从队列中取出产品
System.out.println("Consumer has taken item: " + item);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
});
producer.start();
consumer.start();
}
}
2.9 LinkedListDeque
LinkedListDeque
是 Java 中的一个双端队列(Deque)实现,它基于双向链表。
设计思考:
- 需求场景:
- 在多种编程任务中,需要一个能够快速进行插入和删除操作的双端队列。例如,在实现栈、队列、双向队列等数据结构时,这些操作非常常见。
- 适用于需要快速添加或移除元素的场景,如文字编辑器中的撤销/重做操作、游戏开发中的棋盘移动记录等。
- 现有技术局限性:
ArrayDeque
提供了双端队列的功能,但在进行大量插入和删除操作时,可能需要频繁地扩容和缩容,这会带来额外的性能开销。LinkedList
提供了链表的功能,支持快速的插入和删除操作,但随机访问性能较差。
- 技术融合:
- LinkedListDeque 结合了链表的快速插入和删除特性,使用
LinkedList
的双向链表结构来存储元素,并提供了在链表两端快速添加或移除元素的能力。
- LinkedListDeque 结合了链表的快速插入和删除特性,使用
- 设计理念:
- LinkedListDeque 提供了一个能够在两端快速插入和删除的双端队列,同时保持了链表的灵活性和动态大小调整的能力。
- 实现方式:
- LinkedListDeque 内部使用
LinkedList
来存储元素,利用链表的双向链接特性,允许从两端快速地添加或移除元素,特别适合实现双端队列。
- LinkedListDeque 内部使用
2.9.1 数据结构
图说明:
- LinkedListDeque:
- 表示
LinkedListDeque
类的实例,是一个基于双向链表实现的双端队列。
- 表示
- Head Pointer:
- 头指针,指向双向链表的第一个元素节点。
- Tail Pointer:
- 尾指针,指向双向链表的最后一个元素节点。
- Node1 & Node2:
- 表示双向链表中的两个具体节点,每个节点都包含数据和两个引用,分别指向前一个节点和后一个节点。
- Data1 & Data2:
- 表示两个节点中存储的数据。
- Previous Reference:
Node2
的previous
引用,指向Node1
。
- Next Reference:
Node1
的next
引用,指向Node2
。
2.9.2 执行流程
图说明:
- 创建 LinkedListDeque 实例:初始化
LinkedListDeque
对象。 - 入队操作(offer) :执行将元素添加到双端队列尾部的操作。
- 检查尾部:检查双端队列尾部是否有足够空间添加新节点。
- 在尾部添加节点:在双端队列尾部添加新节点。
- 更新尾指针:更新尾指针以指向新节点。
- 入队完成:完成入队操作。
- 出队操作(poll) :执行将元素从双端队列头部移除的操作。
- 检查头部:检查双端队列头部是否有节点。
- 在头部移除节点:从双端队列头部移除节点。
- 更新头指针:更新头指针以指向新的头部节点。
- 出队完成:完成出队操作。
- 头部查看(peek) :查看双端队列头部的元素而不移除它。
- 读取头部节点数据:读取头部节点的数据。
- 尾部查看(peekLast) :查看双端队列尾部的元素而不移除它。
- 读取尾部节点数据:读取尾部节点的数据。
2.9.3 锁流程
图说明:
- 创建 LinkedListDeque 实例:初始化
LinkedListDeque
对象。 - 生产者线程到达:生产者线程准备向队列中添加元素。
- 消费者线程到达:消费者线程准备从队列中移除元素。
- 队列为空:检查队列是否为空。
- 队列非空:检查队列是否非空。
- 生产者获取锁:生产者线程获取锁以确保线程安全地访问队列。
- 消费者获取锁:消费者线程获取锁以确保线程安全地访问队列。
- 生产者等待:如果队列已满,生产者线程将在
notFull
条件变量上等待。 - 消费者等待:如果队列为空,消费者线程将在
notEmpty
条件变量上等待。 - 生产者入队操作:生产者线程向队列中添加元素。
- 消费者出队操作:消费者线程从队列中移除元素。
- 生产者唤醒消费者:生产者线程完成入队操作后,唤醒等待的消费者线程。
- 消费者唤醒生产者:消费者线程完成出队操作后,唤醒等待的生产者线程。
- 操作完成:入队或出队操作完成。
- 点赞
- 收藏
- 关注作者
评论(0)