一、实现原理
⚠️注意
✔️有界阻塞队列:容量固定,必须在初始化时指定长度,无自动扩容机制。
✔️先进先出(FIFO) :入队元素从队尾添加,出队元素从队首取出。
✔️存取互斥:所有读写操作共享同一把ReentrantLock 锁,同一时间只能执行入队或出队操作。
二,使用场景
1、流控例子🌰🌰🌰
package com.nl;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeUnit;
/**
* 基于阻塞队列的高并发流量控制
* 作用:限制同时进入系统的请求数,保护系统不被压垮
*/
public class FlowControl {
// 队列容量 = 最大允许并发排队的请求数(流控核心)
private static final int MAX_QUEUE_SIZE = 2;
// 阻塞队列(线程安全,自动实现阻塞/唤醒)
private static final BlockingQueue<String> QUEUE = new ArrayBlockingQueue<>(MAX_QUEUE_SIZE);
public static void main(String[] args) {
System.out.println("系统启动,最大并发排队请求:" + MAX_QUEUE_SIZE);
// 消费者:固定线程数处理请求(保护系统!)
// 这里只开 1 个处理线程,你可以根据系统能力开 3/5/10 个
new Thread(FlowControl::handleRequest, "Consumer-Thread").start();
// 生产者:模拟高并发请求(10个并发请求)
for (int i = 1; i <= 5; i++) {
final int requestNo = i;
new Thread(() -> {
try {
// 关键:队列满了会自动阻塞等待 → 真正限流
QUEUE.put("Request-" + requestNo);
// 阻塞 1 秒,丢弃请求
// boolean success = QUEUE.offer("Request-" + requestNo, 1, TimeUnit.SECONDS);
// if (!success) {
// System.out.println("请求过多,系统繁忙,拒绝请求:" + requestNo);
// }
System.out.println("【入队】" + Thread.currentThread().getName() + " → " + "Request-" + requestNo);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}, "Producer-" + i).start();
}
}
/**
* 消费处理请求(核心业务)
*/
private static void handleRequest() {
while (true) {
try {
// 关键:队列空会自动阻塞,不消耗CPU
String request = QUEUE.take();
// 模拟业务处理(如接口调用、数据库操作)
System.out.println("【处理】" + Thread.currentThread().getName() + " 处理 → " + request);
Thread.sleep(500);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
break;
}
}
}
}
2、输出结果
系统启动,最大并发排队请求:2
【入队】Producer-1 → Request-1
【入队】Producer-4 → Request-4
【处理】Consumer-Thread 处理 → Request-1
【入队】Producer-2 → Request-2
【处理】Consumer-Thread 处理 → Request-4
【入队】Producer-3 → Request-3
【处理】Consumer-Thread 处理 → Request-2
【入队】Producer-5 → Request-5
【处理】Consumer-Thread 处理 → Request-3
【处理】Consumer-Thread 处理 → Request-5
⚠️注意
✔️阻塞队列是高并发限流、削峰、保护系统的最简单高效方案
✔️请求不会冲垮系统,CPU / 线程 / 内存都可控
三、源码分析
1、构造函数
public class ArrayBlockingQueue<E> extends AbstractQueue<E>
implements BlockingQueue<E>, java.io.Serializable {
......
//数据元素数组
final Object[] items;
//下一个待取出元素索引
int takeIndex;
//下一个待添加元素索引
int putIndex;
//数组元素个数
int count;
//ReentrantLock 内部锁
final ReentrantLock lock;
//消费者条件
private final Condition notEmpty;
//生产者条件
private final Condition notFull;
public ArrayBlockingQueue(int capacity) {
this(capacity, false);
}
//
public ArrayBlockingQueue(int capacity, boolean fair) {
if (capacity <= 0)
throw new IllegalArgumentException();
this.items = new Object[capacity];
lock = new ReentrantLock(fair);
notEmpty = lock.newCondition();
notFull = lock.newCondition();
}
......
}
2、ReentrantLock:并发控制
// 出队
public E take() throws InterruptedException {
// ReentrantLock
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
//count判断
while (count == 0)
notEmpty.await();
return dequeue();
} finally {
lock.unlock();
}
}
// 入队
public void put(E e) throws InterruptedException {
checkNotNull(e);
final ReentrantLock lock = this.lock;
lock.lockInterruptibly();
try {
while (count == items.length)
notFull.await();
enqueue(e);
} finally {
lock.unlock();
}
}
- 锁机制:使用
ReentrantLock保证线程安全,入队和出队操作共用同一把锁,实现完全互斥。 - 阻塞等待:
-
notEmpty:当队列为空(count=0)时,出队线程会阻塞在该对象上,等待新元素入队。notFull:当队列已满(count=length)时,入队线程会阻塞在该对象上,等待队列腾出空位。
3、数据结构
3.1、数组Object[]
final Object[] items = this.items;
3.2、入队和出队
// 出队
private E dequeue() {
// assert lock.getHoldCount() == 1;
// assert items[takeIndex] != null;
final Object[] items = this.items;
@SuppressWarnings("unchecked")
E x = (E) items[takeIndex];
items[takeIndex] = null;
if (++takeIndex == items.length)
takeIndex = 0;
count--;
if (itrs != null)
itrs.elementDequeued();
notFull.signal();
return x;
}
// 入队
private void enqueue(E x) {
// assert lock.getHoldCount() == 1;
// assert items[putIndex] == null;
final Object[] items = this.items;
items[putIndex] = x;
if (++putIndex == items.length)
putIndex = 0;
count++;
notEmpty.signal();
}
⚠️注意
✔️底层采用静态数组实现。
✔️数组中无元素的位置会被 null 占位,因此空间利用率固定。
4、双指针设计
4.1、入队(put/offer)
- 从
putIndex位置开始添加元素。 putIndex自增,到达数组末尾时重置为 0(循环数组)。- 元素入队成功后,唤醒阻塞在
notEmpty上的出队线程。
4.2、出队(take/poll) :
- 从
takeIndex位置开始取出元素。 takeIndex自增,到达数组末尾时重置为 0(循环数组)。- 元素出队成功后,唤醒阻塞在
notFull上的入队线程。
⚠️指针设计
✔️putIndex和takeIndex均从队首向队尾循环移动,严格保证 FIFO 顺序。
✔️ArrayBlockingQueue的双指针(putIndex和takeIndex)是为了在静态数组上高效实现循环队列(环形缓冲区) 。
5、固定长度的静态数组
- 底层是固定长度的静态数组,没有扩容机制。
- 用
putIndex记录下一个入队元素要存放的位置,用takeIndex记录下一个出队元素要取出的位置。 - 两个指针都从 0 开始向后移动,到达数组末尾(
index == array.length)时重置为 0,形成逻辑上的环形复用,避免数组空间浪费。
三、索引变化逻辑:为什么初始都是 0,put 和 take 会不会冲突?
两个索引都从 0 开始,put 和 take 不会冲突核心原因是 count 计数 + 锁的互斥性 保证了安全
四、总结
- 优势:有界设计避免了内存溢出风险,锁和条件变量的组合实现了高效的生产者 - 消费者模式。
- 局限:容量固定无法动态扩展,独占锁ReentrantLock实现线程安全,入队和出队操作使用同一个锁对象,在高并发场景下会成为性能瓶颈。