浅谈阻塞队列: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,再解决无上限不扩容的问题,慢慢摸到了现在的主流配置:合理容量 + 灵活扩容。

相关推荐
uzong21 分钟前
技术故障复盘模版
后端
GetcharZp1 小时前
基于 Dify + 通义千问的多模态大模型 搭建发票识别 Agent
后端·llm·agent
桦说编程1 小时前
Java 中如何创建不可变类型
java·后端·函数式编程
IT毕设实战小研1 小时前
基于Spring Boot 4s店车辆管理系统 租车管理系统 停车位管理系统 智慧车辆管理系统
java·开发语言·spring boot·后端·spring·毕业设计·课程设计
wyiyiyi2 小时前
【Web后端】Django、flask及其场景——以构建系统原型为例
前端·数据库·后端·python·django·flask
阿华的代码王国3 小时前
【Android】RecyclerView复用CheckBox的异常状态
android·xml·java·前端·后端
Jimmy3 小时前
AI 代理是什么,其有助于我们实现更智能编程
前端·后端·ai编程
AntBlack3 小时前
不当韭菜V1.1 :增强能力 ,辅助构建自己的交易规则
后端·python·pyqt
bobz9654 小时前
pip install 已经不再安全
后端
寻月隐君4 小时前
硬核实战:从零到一,用 Rust 和 Axum 构建高性能聊天服务后端
后端·rust·github