高并发接口总被打崩?我用 ArrayBlockingQueue + 底层源码深度剖析搞定流控

一、实现原理

⚠️注意

✔️有界阻塞队列 :容量固定,必须在初始化时指定长度,无自动扩容机制

✔️先进先出(FIFO) :入队元素从队尾添加,出队元素从队首取出。

✔️存取互斥:所有读写操作共享同一把ReentrantLock 锁,同一时间只能执行入队或出队操作。


二,使用场景

1、流控例子🌰🌰🌰

java 复制代码
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、输出结果

vbscript 复制代码
系统启动,最大并发排队请求: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、构造函数

csharp 复制代码
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:并发控制

csharp 复制代码
    // 出队
    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[]

ini 复制代码
final Object[] items = this.items;

3.2、入队和出队

ini 复制代码
    // 出队
    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)

  1. putIndex 位置开始添加元素。
  2. putIndex 自增,到达数组末尾时重置为 0(循环数组)。
  3. 元素入队成功后,唤醒阻塞在 notEmpty 上的出队线程。

4.2、出队(take/poll)

  1. takeIndex 位置开始取出元素。
  2. takeIndex 自增,到达数组末尾时重置为 0(循环数组)。
  3. 元素出队成功后,唤醒阻塞在 notFull 上的入队线程。

⚠️指针设计

✔️putIndextakeIndex 均从队首向队尾循环移动,严格保证 FIFO 顺序。

✔️ArrayBlockingQueue 的双指针(putIndextakeIndex)是为了在静态数组 上高效实现循环队列(环形缓冲区)

5、固定长度的静态数组

  • 底层是固定长度的静态数组,没有扩容机制。
  • putIndex 记录下一个入队元素要存放的位置 ,用 takeIndex 记录下一个出队元素要取出的位置
  • 两个指针都从 0 开始向后移动,到达数组末尾(index == array.length)时重置为 0,形成逻辑上的环形复用,避免数组空间浪费。

三、索引变化逻辑:为什么初始都是 0,put 和 take 会不会冲突?

两个索引都从 0 开始,put 和 take 不会冲突核心原因是 count 计数 + 锁的互斥性 保证了安全


四、总结

  • 优势 :有界设计避免了内存溢出风险,锁和条件变量的组合实现了高效的生产者 - 消费者模式
  • 局限:容量固定无法动态扩展,独占锁ReentrantLock实现线程安全,入队和出队操作使用同一个锁对象,在高并发场景下会成为性能瓶颈。

关于阻塞队列的实际应用,欢迎在评论区一起讨论~~

相关推荐
木易 士心2 小时前
MyBatis Plus 核心功能与用法
java·后端·mybatis
Victor3562 小时前
MongoDB(93)如何使用变更流跟踪数据变化?
后端
用户6757049885022 小时前
全网都在推 Claude Code,但只有这篇文章教你如何“真正”能用
后端·aigc·claude
Victor3562 小时前
MongoDB(94)什么是MongoDB Atlas?
后端
苏三说技术2 小时前
为什么越来越多的大厂抛弃MCP,转向CLI?
后端
Rust研习社2 小时前
Rust 写时克隆智能指针 Cow
后端·rust·编程语言
董董灿是个攻城狮2 小时前
库克不再担任苹果 CEO,附全员信
后端
伞伞悦读2 小时前
Docker 安装 Redis 教程(重点避坑版)
后端
伞伞悦读2 小时前
Docker 从 C 盘迁移到 D 盘使用教程(Windows + WSL2 + Docker Desktop)
后端