浅谈阻塞队列:Array和Linked与线程池的有机结合

大家好,今天我们来聊聊 Java 中的阻塞队列(BlockingQueue)和它在线程池里的那些事儿。从最基础的概念入手,我们会一步步剖析阻塞队列跟普通队列的关系、常见的两种阻塞队列在加锁和长度上的区别,以及它们在不同线程池中的表现。最终,我们会从一个简单的方案出发,发现问题,再逐步优化,靠近现在最靠谱的线程池配置方式。走起!


1. 阻塞队列和普通队列啥关系?算单列集合吗?

队列是个啥?

说到队列(Queue),简单讲就是个"先来先走"的数据结构,英文叫 FIFO(First In, First Out)。Java 里有个 Queue 接口,定义了基本的操作,比如塞东西(add, offer)、取东西(poll)啥的,大家应该都不陌生。

阻塞队列又是什么?

阻塞队列(BlockingQueue)是 Queue 的"升级版",多了个"堵车"功能。啥意思呢?

  • 如果队列满了,你再往里塞东西,操作就得停下来等着,直到有空位。
  • 如果队列空了,你想拿东西,也得等着,直到有货。

简单来说,它就是在普通队列的基础上加了"耐心等待"的特性,特别适合多线程环境下生产者和消费者之间的协作。

单列集合吗?

是的,阻塞队列属于单列集合。Java 的集合框架分两大家族:

  • Collection:单列集合,比如 List、Set、Queue,存的是一条条独立的数据。
  • Map:双列集合,存的是键值对。

阻塞队列是从 Queue 继承来的,而 QueueCollection 的子接口,所以它妥妥是个单列集合,没跑儿。


2. ArrayBlockingQueue 和 LinkedBlockingQueue 有啥不一样?

Java 里最常见的两种阻塞队列就是 ArrayBlockingQueueLinkedBlockingQueue,咱们从加锁和队列长度两方面掰扯掰扯它们的区别。

ArrayBlockingQueue:数组派

  • 底层咋实现的:用数组存数据。
  • 队列长度:有上限,创建时得定好容量,比如 100 个坑,满了就满了。
  • 加锁咋玩的 :用一个锁(ReentrantLock)管住进出,进队和出队得排队干。

特点

因为只有一个锁,塞东西和拿东西不能一块儿来,谁先抢到锁谁干活儿。适合生产者和消费者节奏差不多的场景。

LinkedBlockingQueue:链表派

  • 底层咋实现的:用链表存数据。
  • 队列长度:灵活得很,可以设上限(比如 1000),也可以不设(理论上无限长)。
  • 加锁咋玩的 :用俩锁(两个 ReentrantLock),一个管塞,一个管拿。

特点

进队和出队各有各的锁,能一块儿干活儿,效率高一些。特别适合生产者和消费者速度不匹配的时候。

区别一览

特点 ArrayBlockingQueue LinkedBlockingQueue
底层结构 数组 链表
队列长度 有上限,得定死 可以有上限,也可以无限
锁的数量 一个锁,进出都抢 俩锁,进出分开管
并发效率 进出得排队 进出能同时干

3. 这些阻塞队列在啥样的线程池里混?

线程池(ThreadPoolExecutor)是 Java 里管线程的大管家,阻塞队列就是它的"任务仓库"。不同的队列会让线程池有不同的脾气,咱们看看这俩家伙都咋用。

ArrayBlockingQueue 在线程池里

  • 特点:容量固定,比如设个 100,满了就塞不下了。
  • 适合啥场景:想严格控制任务堆积数量的时候,避免任务多得炸内存。
  • 咋表现:队列满了,新任务来了就按拒绝策略处理(比如直接扔掉或者抛异常)。

LinkedBlockingQueue 在线程池里

  • 特点:可以有上限,也可以没上限。
  • 适合啥场景
    • 没上限:任务多得吓人,又不想拒绝任务时用。
    • 有上限 :跟 ArrayBlockingQueue 差不多,但效率更高。
  • 咋表现
    • 没上限时,任务随便排队,除非核心线程不够用,不然不会加新线程。
    • 有上限时,满了就加线程(到最大线程数为止),再满就拒绝。

现成的线程池例子

Java 自带了几种线程池,队列用得不一样:

  • FixedThreadPool :用 LinkedBlockingQueue(没上限),线程数固定,任务随便排。
  • CachedThreadPool :用 SynchronousQueue(一种不存货的队列),线程数随便涨。
  • SingleThreadExecutor :也用 LinkedBlockingQueue(没上限),但就一个线程干活。

4. 从简单到牛掰:优化线程池的队列选择

先来个简单方案:ArrayBlockingQueue 开干

咱们假设从一个基础的线程池开始:

java 复制代码
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    5, 10, 60, TimeUnit.SECONDS,
    new ArrayBlockingQueue<>(100)
);
  • 核心线程数:5 个
  • 最大线程数:10 个
  • 队列:ArrayBlockingQueue,能装 100 个任务

工作原理

  • 任务来了,先给 5 个核心线程干。
  • 核心线程忙不过来,任务进队列,最多 100 个。
  • 队列满了,再加线程,直到 10 个。
  • 10 个线程还忙不过来,就按拒绝策略处理。

问题在哪儿?

  • 任务多了咋办:队列到 100 就满了,任务堆积受限,容易触发拒绝。
  • 效率咋样:只有一个锁,塞任务和拿任务得排队,忙起来就卡。

不利的地方

  • 任务量大时,队列老满,线程一会儿加一会儿减,太费劲。
  • 一个锁管进出,高峰期容易堵车,效率上不去。

第一步优化:换 LinkedBlockingQueue

试试 LinkedBlockingQueue,双锁效率高:

java 复制代码
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    5, 10, 60, TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(1000)  // 设个 1000 的上限
);
  • 好处:进出任务能一块儿干,效率提升。
  • 容量:1000 个任务的队列,缓冲更多。

还不够?

如果任务量再大点,1000 也不够用,咋办?有人可能会说:"那我干脆用无上限的 LinkedBlockingQueue!"比如:

java 复制代码
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    5, 10, 60, TimeUnit.SECONDS,
    new LinkedBlockingQueue<>()  // 无上限
);

新问题:线程咋不扩了?

你说得没错!用无上限的 LinkedBlockingQueue,队列永远不会满,因为它能一直往里塞任务。线程池的扩容逻辑是这样的:

  • 核心线程忙不过来,任务进队列。
  • 队列满了,才加线程到最大线程数。

但现在队列没上限,任务全堆队列里了,核心线程数(5 个)就一直干活,永远到不了 10 个最大线程数,除非核心线程忙到炸。这时候内存可能先扛不住,任务堆太多,系统直接崩。

咋解决?

  • 设个合理上限:队列得有个边界,比如 1000 或 5000,看任务量和机器内存定。这样任务多了,队列满后线程能扩到 10 个。
  • 换个队列 :用 SynchronousQueue,它不存任务,任务来了直接推给线程,核心线程忙完就加新线程到最大数。

主流玩法:灵活配置

实际中,LinkedBlockingQueue 是常用选手,但得聪明用:

  • 有上限:比如 1000,控制任务堆积,还能让线程扩容。
  • 无上限:任务量波动大、不想丢任务时用,但得配好监控,别让内存爆。
  • 动态调整:根据业务高峰,调核心线程数、最大线程数和队列容量。

优化后的例子

java 复制代码
ThreadPoolExecutor executor = new ThreadPoolExecutor(
    5, 10, 60, TimeUnit.SECONDS,
    new LinkedBlockingQueue<>(1000)  // 平衡容量和扩容
);
  • 任务少时,5 个核心线程干。
  • 任务多到 1000,队列满,加线程到 10 个。
  • 再多就拒绝,保护系统。

再往前一步:因地制宜选队列

  • SynchronousQueue :任务短平快、要立刻响应,比如 CachedThreadPool,线程随便涨。
  • PriorityBlockingQueue:任务有优先级,按重要性排队。
  • 自定义队列:根据业务写个队列,比如限速的、带权重的。

最后唠唠

阻塞队列是线程池的"任务中转站",选对了能让线程池更顺畅。ArrayBlockingQueue 一个锁、容量固定,适合小规模控制;LinkedBlockingQueue 双锁、容量灵活,高并发更给力。从简单的 ArrayBlockingQueue 出发,我们发现容量和效率的短板,换上 LinkedBlockingQueue,再解决无上限不扩容的问题,慢慢摸到了现在的主流配置:合理容量 + 灵活扩容。

相关推荐
Java中文社群1 小时前
面试官:你项目是如何保证高可用的?
java·后端·面试
冲鸭ONE2 小时前
for循环优化方式有哪些?
后端·性能优化
兮动人2 小时前
DBeaver连接OceanBase数据库
后端
刘鹏3782 小时前
深入浅出Java中的CAS:原理、源码与实战应用
后端
Lx3522 小时前
《从头开始学java,一天一个知识点》之:循环结构:for与while循环的使用场景
java·后端
fliter2 小时前
RKE1、K3S、RKE2 三大 Kubernetes 发行版的比较
后端
aloha_2 小时前
mysql 某个客户端主机在短时间内发起了大量失败的连接请求时
后端
程序员爱钓鱼3 小时前
Go 语言高效连接 SQL Server(MSSQL)数据库实战指南
后端·go·sql server
xjz18423 小时前
Java AQS(AbstractQueuedSynchronizer)实现原理详解
后端
Victor3563 小时前
Zookeeper(97)如何在Zookeeper中实现分布式协调?
后端