大家好,今天我们来聊聊 Java 中的阻塞队列(BlockingQueue)和它在线程池里的那些事儿。从最基础的概念入手,我们会一步步剖析阻塞队列跟普通队列的关系、常见的两种阻塞队列在加锁和长度上的区别,以及它们在不同线程池中的表现。最终,我们会从一个简单的方案出发,发现问题,再逐步优化,靠近现在最靠谱的线程池配置方式。走起!
1. 阻塞队列和普通队列啥关系?算单列集合吗?
队列是个啥?
说到队列(Queue),简单讲就是个"先来先走"的数据结构,英文叫 FIFO(First In, First Out)。Java 里有个 Queue
接口,定义了基本的操作,比如塞东西(add
, offer
)、取东西(poll
)啥的,大家应该都不陌生。
阻塞队列又是什么?
阻塞队列(BlockingQueue)是 Queue
的"升级版",多了个"堵车"功能。啥意思呢?
- 如果队列满了,你再往里塞东西,操作就得停下来等着,直到有空位。
- 如果队列空了,你想拿东西,也得等着,直到有货。
简单来说,它就是在普通队列的基础上加了"耐心等待"的特性,特别适合多线程环境下生产者和消费者之间的协作。
单列集合吗?
是的,阻塞队列属于单列集合。Java 的集合框架分两大家族:
- Collection:单列集合,比如 List、Set、Queue,存的是一条条独立的数据。
- Map:双列集合,存的是键值对。
阻塞队列是从 Queue
继承来的,而 Queue
是 Collection
的子接口,所以它妥妥是个单列集合,没跑儿。
2. ArrayBlockingQueue 和 LinkedBlockingQueue 有啥不一样?
Java 里最常见的两种阻塞队列就是 ArrayBlockingQueue
和 LinkedBlockingQueue
,咱们从加锁和队列长度两方面掰扯掰扯它们的区别。
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
,再解决无上限不扩容的问题,慢慢摸到了现在的主流配置:合理容量 + 灵活扩容。