LinkedBlockingQueue
和 ArrayBlockingQueue
是 Java 中常见的 阻塞队列(Blocking Queue)实现,主要用于在生产者-消费者模型中管理任务的队列,它们都实现了 BlockingQueue
接口。虽然这两者都实现了阻塞队列的功能,但是它们的底层实现有很大的不同。
1. LinkedBlockingQueue
的底层实现
LinkedBlockingQueue
是基于 链表(Linked List) 实现的,它内部使用了一个双向链表来存储队列中的元素。
- 底层结构:双向链表(
LinkedList
)。 特点:
- 没有固定大小的限制,除非显式指定队列的容量。
- 如果没有指定队列的容量,则默认情况下容量是
Integer.MAX_VALUE
,即无界队列。 - 如果队列已满,生产者线程会被阻塞,直到有空间可用。
- 如果队列为空,消费者线程会被阻塞,直到有任务可供消费。
底层实现细节:
LinkedBlockingQueue
使用了 两个锁(分别为putLock
和takeLock
),来处理入队和出队操作:putLock
:用于保护入队(put
)操作的线程安全性。takeLock
:用于保护出队(take
)操作的线程安全性。
- 由于队列是基于链表的,元素插入和删除(即入队和出队操作)都具有 O(1) 的时间复杂度。
优点:
- 由于队列是链表结构,可以支持任意数量的任务(只要系统内存允许)。
- 内存分配不需要事先分配一个固定大小的内存块,可以灵活扩展。
缺点:
- 由于需要维护链表的结构,会有一定的内存开销。
- 链表结构可能导致内存访问不如数组结构那样局部性好,可能会影响缓存命中率。
2. ArrayBlockingQueue
的底层实现
ArrayBlockingQueue
是基于 数组(Array) 实现的,它内部维护一个固定大小的数组作为队列的存储结构。
- 底层结构:数组(
Object[]
)。 特点:
- 容量是固定的,创建时需要指定队列的大小。
- 容量达到最大时,生产者线程会被阻塞,直到队列有空间。
- 如果队列为空,消费者线程会被阻塞,直到有元素可供消费。
底层实现细节:
ArrayBlockingQueue
使用了一个环形数组来存储队列元素。- 通过维护两个指针来记录队列的头部和尾部位置。
- 通过 环形数组 实现队列的循环操作,这样就避免了空间浪费。当尾部指针达到数组末尾时,它会跳回到数组的开始部分继续存放元素。
队列的入队和出队操作是通过 数组的索引 来完成的:
put()
和take()
方法通过调整头部和尾部指针来控制队列的状态。
优点:
- 由于底层是数组结构,访问速度非常快,且内存布局比较紧凑,效率较高。
- 内存开销较小,适用于任务数固定且较少的场景。
缺点:
- 队列的容量是固定的,一旦创建,不能动态扩展。
- 如果队列满了,线程会被阻塞,且可能需要等待较长时间。
总结:底层结构对比
特性 | LinkedBlockingQueue | ArrayBlockingQueue |
---|---|---|
底层实现 | 基于 链表(LinkedList) | 基于 数组(Array) |
容量限制 | 默认无界(可以指定容量) | 有界,容量固定 |
插入/删除操作 | 使用链表节点,时间复杂度 O(1) | 使用数组索引,时间复杂度 O(1) |
内存使用 | 内存开销较大,适用于不确定数量的任务 | 内存开销较小,适用于已知固定任务 |
线程同步机制 | 使用两个锁(putLock 和 takeLock ) | 使用单个锁和数组的原子操作 |
优点 | 可以动态扩展,适合任务数不确定的情况 | 内存占用少,性能较好,适用于固定任务数量的场景 |
适用场景
LinkedBlockingQueue
:适用于任务数目不确定或任务数量较多的场景,尤其是当你希望线程池的任务量不受限制时。ArrayBlockingQueue
:适用于任务数目较为稳定且固定的场景,适合负载较轻、且对内存开销较为敏感的应用。
这两者的选择通常根据你的应用需求来决定:如果任务量较大并且不确定,使用 LinkedBlockingQueue
;如果任务数量较为稳定且有界,使用 ArrayBlockingQueue
会更高效。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。