别再乱写并发了!弄懂阻塞队列,解决 90% 线程安全问题

前言

上周,我在消息日志场景用多线程踩了坑:推送消息时频繁出现重复、漏推问题,高峰期还会导致系统卡顿甚至无响应。

经过排查分析,确定问题根源是多线程无序竞争资源,于是改用队列,按照"先到先处理"的规则,让多线程有序获取消息并处理,不仅彻底解决了上述问题,还实现了系统解耦。

一、什么是队列

队列是一种特殊的线性表 ,其操作被严格限定:仅允许在一端进行数据插入,在另一端进行数据删除。它遵循先进先出(FIFO, First In First Out)的原则。其中,允许删除数据的一端称为队头 ,允许插入数据的一端称为队尾

二、阻塞队列

阻塞队列(BlockingQueue)是 java.util.concurrent 包下的核心数据结构。它提供了**线程安全的阻塞式队列操作**:

  • 向队列插入元素时,若队列已满,线程会阻塞等待,直到队列有空闲位置;
  • 从队列获取元素时,若队列为空,线程会阻塞等待,直到队列中有数据可取。

juc 包中许多高级同步工具类,底层都是基于 BlockingQueue 实现的。

1、与普通队列的区别

  • 普通队列:只保证 FIFO 顺序,不处理并发,满了 / 空了直接报错或返回特殊值,不会阻塞线程。
  • 阻塞队列 :在队列基础上增加并发安全 + 阻塞等待机制,满则阻塞写入,空则阻塞读取,比如:生产者 - 消费者模型。

三、BlockingQueue接口

1、接口源码

java 复制代码
public interface BlockingQueue<E> extends Queue<E> {
    // 如果能在不违反容量限制的情况下立即执行,则将指定元素插入此队列,并返回
    boolean add(E e);

    // 如果能在不违反容量限制的情况下立即执行,则将指定元素插入此队列,并在成功时返回true,如果当前没有可用空间,则返回false。在使用容量受限的队列时,此方法通常比add方法更可取,因为add方法在插入元素失败时只会抛出异常。
    boolean offer(E e);

    // 将指定元素插入此队列,必要时等待空间变为可用状态。
    void put(E e) throws InterruptedException;

    // 将指定元素插入此队列,如有必要,可等待指定等待时间以获取可用空间。
    boolean offer(E e, long timeout, TimeUnit unit)
        throws InterruptedException;

    // 检索并移除此队列的头部,如有必要,则等待直至有元素可用。
    E take() throws InterruptedException;

    // 检索并移除此队列的头部,如有必要,可等待指定时间以获取可用元素
    E poll(long timeout, TimeUnit unit)
        throws InterruptedException;

    // 返回此队列在理想情况下(无内存或资源限制时)在不阻塞的情况下可接受的额外元素数量,若无固有上限,则返回{@code Integer.MAX_VALUE}。
    int remainingCapacity();

    // 如果指定元素存在于此队列中,则从此队列中移除该元素的一个实例。更正式地说,如果此队列包含一个或多个满足条件(即 o.equals(e))的元素,则移除满足该条件的元素 e。
    boolean remove(Object o);

    // 如果此队列包含指定的元素,则返回{@code true}。更正式地说,当且仅当此队列至少包含一个满足条件(即{@code o.equals(e)})的元素{@code e}时,才返回{@code true}。
    public boolean contains(Object o);

    // 从此队列中移除所有可用元素,并将其添加到给定的集合中。此操作可能比反复轮询此队列更高效。在尝试向集合{@code c}添加元素时遇到故障,可能导致在抛出相关异常时,元素既不在此队列中,也不在给定集合中,或者同时存在于两者中。尝试将队列清空到自身会导致{@code IllegalArgumentException}。此外,如果在操作进行过程中修改了指定的集合,则此操作的行为将未定义。
    int drainTo(Collection<? super E> c);
    
    // 从该队列中最多移除给定数量的可用元素,并将其添加到给定的集合中。在尝试向集合{@code c}添加元素时遇到的失败可能会导致在抛出相关异常时,元素既不在其中一个集合中,也不在两个集合中。尝试将队列清空到自身会导致{@code IllegalArgumentException}。此外,如果在操作进行过程中修改了指定的集合,则此操作的行为是未定义的。
    int drainTo(Collection<? super E> c, int maxElements);
}

2、汇总

方法 抛出异常 返回特定值 阻塞 阻塞特定时间
入队 add(e) offer(e) put(e) offer(e, time, unit)
出队 remove() poll() take() poll(time, unit)
获取队首元素 element() peek() 不支持 不支持

四、应用场景

阻塞队列在实际应用中有很多场景,以下是一些常见的应用场景:

1、线程池

线程池内部的任务队列,一般采用阻塞队列实现。

  • 当任务提交量超出线程池可同时处理的上限时,新任务会被放入任务队列中等待调度。工作线程持续从任务队列中获取任务执行;
  • 若队列为空,工作线程将进入阻塞状态,直至有新任务被提交至队列。

2、生产者-消费者模型

在生产者 - 消费者模型中,生产者负责向队列中添加元素,消费者则从队列中取出元素并进行处理。阻塞队列能够高效解决生产者与消费者之间的并发协作问题,有效避免线程间的资源竞争与执行冲突。

3、缓存系统

缓存系统借助阻塞队列存储缓存数据,当缓存数据发生更新时,会将最新数据写入队列,其他线程可从队列中获取最新数据进行使用。采用阻塞队列能够有效避免多线程并发更新缓存时产生的资源竞争与数据冲突。

4、并发任务处理

在并发任务处理场景中,待处理任务会被提交至阻塞队列中,由多个工作线程从队列中获取并执行。阻塞队列能有效避免多线程争抢同一任务的问题,同时实现任务提交与执行的解耦,从而提升系统的可维护性与可扩展性

五、总结

综上可见,阻塞队列作为并发处理的核心基础组件,广泛应用于线程池、生产者 - 消费者模型、消息队列与缓存同步等场景。它通过阻塞机制协调生产与消费速率,由生产者提交任务,消费者阻塞获取并执行,从根源上避免多线程资源竞争。

这也正是我在消息日志场景中解决推送重复、漏推与系统卡顿的关键 ------ 依靠队列实现多线程有序处理,既保证了并发安全,又实现了任务提交与执行的解耦,最终显著提升系统的吞吐量、响应速度与稳定性

关于阻塞队列的实际应用,你还有哪些经验或疑问?欢迎在评论区一起讨论。

相关推荐
敖正炀2 小时前
线程池决绝策略
后端
Moe4882 小时前
WebSocket :从浏览器 API 到 Spring 握手、Handler 与前端客户端
java·后端·架构
神奇小汤圆2 小时前
探索springboot程序打包docker的最佳方式
后端
邦爷的AI架构笔记2 小时前
我用Claude API接入了CI/CD安全扫描,踩了这几个坑
后端
henujolly3 小时前
go学习第一天
后端
毕业设计-小慧3 小时前
计算机毕业设计springboot城市休闲垂钓园管理系统 基于Spring Boot的都市休闲垂钓基地数字化运营平台 城市智慧钓场综合服务管理平台
spring boot·后端·课程设计
Nyarlathotep01133 小时前
ReentrantReadWriteLock基础和原理
java·后端
woniu_maggie4 小时前
SAP CPI 开发RFC适配器的Integration Flow
后端