高并发接口总被打崩?我用 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实现线程安全,入队和出队操作使用同一个锁对象,在高并发场景下会成为性能瓶颈。

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

相关推荐
无风听海8 小时前
深入剖析 YARP 的 Transforms:构建灵活的反向代理转换管道
后端·中间件·asp.net
Gopher_HBo8 小时前
负载均衡
后端
自由生长20248 小时前
RAG已死?什么标题党啊!
后端
东方小月9 小时前
5分钟搞懂Harness Engineering(驾驭工程):从提示词到AI Agent的进化之路
前端·后端·架构
折哥的程序人生 · 物流技术专研12 小时前
Java面试85题图解版(一):基础核心篇
java·开发语言·后端·面试
Moment12 小时前
面试官:如果产品经理给你多个需求,怎么让AI去完成❓❓❓
前端·后端·面试
每天进步一点_JL13 小时前
JVM 内存模型与 OOM 排查:从入门到实战
后端
REDcker13 小时前
个人博客网站建设指南 Markdown资产化与静态站选型部署
前端·后端·博客·markdown·网站·资产·建站
Supersist14 小时前
【设计模式03】使用模版模式+责任链模式优化实战
后端·设计模式·代码规范
Fox爱分享14 小时前
字节二面:10亿数据毫秒级查手机尾号后4位,答不出“异构索引”直接挂?
java·后端·面试