LinkedBlockingQueueArrayBlockingQueue 是 Java 中常见的 阻塞队列(Blocking Queue)实现,主要用于在生产者-消费者模型中管理任务的队列,它们都实现了 BlockingQueue 接口。虽然这两者都实现了阻塞队列的功能,但是它们的底层实现有很大的不同。

1. LinkedBlockingQueue 的底层实现

LinkedBlockingQueue 是基于 链表(Linked List) 实现的,它内部使用了一个双向链表来存储队列中的元素。

  • 底层结构:双向链表(LinkedList)。
  • 特点

    • 没有固定大小的限制,除非显式指定队列的容量。
    • 如果没有指定队列的容量,则默认情况下容量是 Integer.MAX_VALUE,即无界队列。
    • 如果队列已满,生产者线程会被阻塞,直到有空间可用。
    • 如果队列为空,消费者线程会被阻塞,直到有任务可供消费。

底层实现细节

  • LinkedBlockingQueue 使用了 两个锁(分别为 putLocktakeLock),来处理入队和出队操作:

    • putLock:用于保护入队(put)操作的线程安全性。
    • takeLock:用于保护出队(take)操作的线程安全性。
  • 由于队列是基于链表的,元素插入和删除(即入队和出队操作)都具有 O(1) 的时间复杂度。

优点

  • 由于队列是链表结构,可以支持任意数量的任务(只要系统内存允许)。
  • 内存分配不需要事先分配一个固定大小的内存块,可以灵活扩展。

缺点

  • 由于需要维护链表的结构,会有一定的内存开销。
  • 链表结构可能导致内存访问不如数组结构那样局部性好,可能会影响缓存命中率。

2. ArrayBlockingQueue 的底层实现

ArrayBlockingQueue 是基于 数组(Array) 实现的,它内部维护一个固定大小的数组作为队列的存储结构。

  • 底层结构:数组(Object[])。
  • 特点

    • 容量是固定的,创建时需要指定队列的大小。
    • 容量达到最大时,生产者线程会被阻塞,直到队列有空间。
    • 如果队列为空,消费者线程会被阻塞,直到有元素可供消费。

底层实现细节

  • ArrayBlockingQueue 使用了一个环形数组来存储队列元素。

    • 通过维护两个指针来记录队列的头部和尾部位置。
    • 通过 环形数组 实现队列的循环操作,这样就避免了空间浪费。当尾部指针达到数组末尾时,它会跳回到数组的开始部分继续存放元素。
  • 队列的入队和出队操作是通过 数组的索引 来完成的:

    • put()take() 方法通过调整头部和尾部指针来控制队列的状态。

优点

  • 由于底层是数组结构,访问速度非常快,且内存布局比较紧凑,效率较高。
  • 内存开销较小,适用于任务数固定且较少的场景。

缺点

  • 队列的容量是固定的,一旦创建,不能动态扩展。
  • 如果队列满了,线程会被阻塞,且可能需要等待较长时间。

总结:底层结构对比

特性LinkedBlockingQueueArrayBlockingQueue
底层实现基于 链表(LinkedList)基于 数组(Array)
容量限制默认无界(可以指定容量)有界,容量固定
插入/删除操作使用链表节点,时间复杂度 O(1)使用数组索引,时间复杂度 O(1)
内存使用内存开销较大,适用于不确定数量的任务内存开销较小,适用于已知固定任务
线程同步机制使用两个锁(putLocktakeLock使用单个锁和数组的原子操作
优点可以动态扩展,适合任务数不确定的情况内存占用少,性能较好,适用于固定任务数量的场景

适用场景

  • LinkedBlockingQueue:适用于任务数目不确定或任务数量较多的场景,尤其是当你希望线程池的任务量不受限制时。
  • ArrayBlockingQueue:适用于任务数目较为稳定且固定的场景,适合负载较轻、且对内存开销较为敏感的应用。

这两者的选择通常根据你的应用需求来决定:如果任务量较大并且不确定,使用 LinkedBlockingQueue;如果任务数量较为稳定且有界,使用 ArrayBlockingQueue 会更高效。


今夜有点儿凉
40 声望1 粉丝

今夜有点儿凉,乌云遮住了月亮。


引用和评论

0 条评论