【JUC】并发容器:从线程不安全到阻塞队列

目录

一、传统容器线程安全吗?

[第一代补丁:同步容器(Vector / Hashtable)](#第一代补丁:同步容器(Vector / Hashtable))

二、常见并发容器

ConcurrentHashMap:锁的粒度演进

CopyOnWriteArrayList:写时复制

三、阻塞队列

什么是阻塞队列

[四组 API:一个表格记全](#四组 API:一个表格记全)

常见实现

和线程池的关系

四、总结


锁和原子类解决的是"怎么保护共享数据",这一篇上升到容器层面:Java 集合在并发下的安全演进------传统容器的坑 → 同步容器的低效 → 并发容器的高效设计,最后落到线程池的基石:阻塞队列

一、传统容器线程安全吗?

不安全。 ArrayListHashMapLinkedList 等日常容器都没做任何并发控制:

复制代码
List<Integer> list = new ArrayList<>();
for (int i = 0; i < 100; i++) {
    new Thread(() -> {
        for (int j = 0; j < 1000; j++) {
            list.add(j);   // 多线程 add:可能丢数据、抛 ArrayIndexOutOfBoundsException
        }
    }).start();
}

多线程 add 出问题的原因:add 内部先判断容量、必要时扩容(复制数组)、再赋值------三步非原子,并发时可能覆盖元素扩容错位导致数组越界

迭代时也不安全:一边遍历一边被其他线程 remove,抛 ConcurrentModificationException(fail-fast,靠 modCount 比对)。

第一代补丁:同步容器(Vector / Hashtable)

JDK 1.0 时代的 VectorHashtable 直接给每个方法加 synchronized

复制代码
public synchronized boolean add(E e) { ... }   // Vector 的 add

两个硬伤

  1. 性能差:读读也互斥------100 个线程读 Vector,也得排队;

  2. 组合操作依然不安全

    if (!vector.contains(x)) { // 检查:持锁
    vector.add(x); // add:重新持锁 ------ 两步之间锁已释放!
    }
    // "检查再执行"不是原子的,照样可能重复添加

结论:同步容器只是"单个方法原子",容器级操作仍需外部加锁,且锁粒度太粗。这就是 JDK 5 引入并发容器的原因。

二、常见并发容器

Java 5 起在 java.util.concurrent 包提供新一代容器,设计思路不再是"大锁包一切",而是缩小锁范围或干脆无锁

容器 替代对象 核心设计
ConcurrentHashMap HashMap CAS + synchronized 锁单个桶(JDK 8)
CopyOnWriteArrayList ArrayList 写时复制,读完全无锁
CopyOnWriteArraySet HashSet 内部就是 CopyOnWriteArrayList
ConcurrentLinkedQueue Queue CAS 无锁队列(Michael-Scott 算法)
ConcurrentSkipListMap TreeMap 跳表实现,天然支持并发有序
BlockingQueue 家族 Queue 阻塞语义,见第三节

ConcurrentHashMap:锁的粒度演进

  • JDK 1.7分段锁(Segment)------把整个 Map 切成 16 段,每段一把锁,不同段并发写入互不影响;
  • JDK 1.8 :放弃分段,改为 CAS + synchronized 锁单个桶头节点 ------哈希越分散,冲突越少,理论并发度从 16 提升到桶的数量级别;size 统计用 baseCount + CounterCell[] 分散计数(与 LongAdder 同款思路)。

详尽版见本系列《ConcurrentHashMap》专篇,这里记住演进逻辑:锁的粒度不断缩小

CopyOnWriteArrayList:写时复制

读操作完全无锁 ,写操作加锁后复制整个数组、在副本上修改、再原子替换引用:

复制代码
public boolean add(E e) {
    synchronized (lock) {
        Object[] es = getArray();
        int len = es.length;
        es = Arrays.copyOf(es, len + 1);  // 复制新数组
        es[len] = e;
        setArray(es);                     // 原子替换引用(volatile)
        return true;
    }
}
  • :无锁,且读到的是某一时刻的完整快照,迭代不抛 CME;
  • :复制全数组,代价 O(n)。

适用场景:读极多、写极少、列表不大(如监听器列表、配置缓存)。写频繁时内存复制开销和旧数据一致性(弱一致)都不可接受。

三、阻塞队列

什么是阻塞队列

BlockingQueue 在普通队列语义上增加两条规则:

  • 队列满时,put 的线程阻塞等待(而不是报错);
  • 队列空时,take 的线程阻塞等待(而不是返回 null)。

这一个特性天然匹配生产者-消费者模型------生产消费速率不一致时,阻塞队列就是中间的"蓄水池",还自带线程安全:

复制代码
BlockingQueue<Integer> queue = new ArrayBlockingQueue<>(10);

new Thread(() -> {                 // 生产者
    try {
        queue.put(produce());     // 满了自动阻塞
    } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}).start();

new Thread(() -> {                 // 消费者
    try {
        consume(queue.take());    // 空了自动阻塞
    } catch (InterruptedException e) { Thread.currentThread().interrupt(); }
}).start();

对比上一篇手写生产者-消费者还要 lock + 双 Condition + while 循环------阻塞队列直接把这套同步逻辑封装好了

四组 API:一个表格记全

BlockingQueue 对"插入/移除/检查"各提供四种失败处理方式:

操作方式 抛异常 返回特殊值 一直阻塞 超时退出
插入 add(e) offer(e) put(e) offer(e, time, unit)
移除 remove() poll() take() poll(time, unit)
检查 element() peek() --- ---

记忆:抛异常的用在"不该失败的地方" (队列状态由你保证);阻塞的用在生产者-消费者超时的用在不想无限等的场景

常见实现

实现类 结构 特点
ArrayBlockingQueue 数组 + 一把锁 有界,必须指定容量,读写共用一把锁
LinkedBlockingQueue 链表 + 两把锁(读写分离) 默认无界(Integer.MAX_VALUE),读写各一把锁,吞吐更高
SynchronousQueue 不存储元素 容量为 0:put 必须等 take 来"交接",一手交一手
PriorityBlockingQueue 按优先级出队,无界
DelayQueue 元素到期才能取出,定时任务场景

和线程池的关系

ThreadPoolExecutor 的任务缓冲区就是一个 BlockingQueue------这正是构造线程池时必填 workQueue 参数的原因:

复制代码
new ThreadPoolExecutor(
    corePoolSize, maximumPoolSize, keepAliveTime, unit,
    workQueue,            // ArrayBlockingQueue / LinkedBlockingQueue / SynchronousQueue
    threadFactory, handler);
  • Executors.newFixedThreadPool → 无界 LinkedBlockingQueue(任务堆积有 OOM 风险);
  • Executors.newCachedThreadPoolSynchronousQueue(不缓存,来了就交给线程);
  • 阿里规约推荐手动 new ThreadPoolExecutor 并使用有界队列,就是为了防止无界队列悄悄吃掉内存。

四、总结

知识点 一句话记忆
传统容器 ArrayList/HashMap 并发下会丢数据、越界、CME
同步容器 Vector 方法级 synchronized:读读互斥性能差,组合操作仍不安全
并发容器演进 锁粒度不断缩小:整锁 → 分段 → 单桶 → 无锁
ConcurrentHashMap 1.7 分段锁,1.8 CAS + synchronized 锁单桶
CopyOnWriteArrayList 读无锁读快照,写复制全数组------读多写少专用
阻塞队列 满了 put 阻塞、空了 take 阻塞,生产者-消费者的现成方案
四组 API add/offer/put/offer(timeout),按"失败怎么办"选
与线程池 线程池的任务缓冲就是 BlockingQueue,建议用有界队列防 OOM

至此 JUC 主线(并发基础 → 锁 → AQS/原子类 → 并发容器)走完,后面的线程池、Future 体系都建立在这几块之上。

相关推荐
许彰午2 小时前
50-18个表单控件
java·低代码·架构
白宇横流学长2 小时前
基于springboot的个人博客系统的设计与实现【源码+文档】
java·spring boot
白远山3 小时前
本地游戏代练源码开发实战:架构设计与核心功能实现指南
java·开发语言·架构·需求分析
wno7043 小时前
Spring Security添加图形验证码
java·后端·spring
曹牧3 小时前
Postman:`Content-Type` 和 `charset`
java
逆境不可逃3 小时前
Pi Agent 学习笔记:扩展如何介入工具调用
java
用户094248568033 小时前
第12章:JDK 诊断工具箱——jps / jstat / jmap / jcmd / jhsdb
java·jvm
景熙55233 小时前
14.Java 集合框架从入门到源码万字详解(含 ArrayList/LinkedList/HashMap 源码、泛型通配符、红黑树)
java·开发语言·数据结构·算法
五仁火烧4 小时前
大模型开发: MCP
java·大模型·mcp