Java 集合框架源码解析与并发容器实战

深入 Java 集合框架底层:ArrayList/LinkedList 扩容机制、HashMap 与红黑树、ConcurrentHashMap 锁分段与 CAS、CopyOnWriteArrayList、队列家族与并发容器选型,以及迭代器 fail-fast 原理与工程选型指南。

集合框架是 Java 使用率最高的库之一,也是面试与性能调优的常客。本文深入 JDK 源码,讲清每个容器的数据结构、扩容时机、线程安全性,并给出高并发场景的选型结论——记住"它为什么这样设计",远比记住"怎么用"值钱。

一、List 家族:ArrayList 与 LinkedList

1.1 ArrayList:动态数组

底层是 Object[] elementData,默认容量 10,扩容按 1.5 倍(oldCapacity + (oldCapacity >> 1)),扩容 = 新建数组 + System.arraycopy 拷贝。

// 扩容核心逻辑(JDK17 简化)
private Object[] grow(int minCapacity) {
    int oldCapacity = elementData.length;
    int newCapacity = oldCapacity + (oldCapacity >> 1);  // 1.5 倍
    return elementData = Arrays.copyOf(elementData, newCapacity);
}

复杂度对照:

操作平均最坏
尾部 addO(1) 摊还O(n)(扩容时)
指定位置插入/删除O(n)O(n)
随机访问 get(i)O(1)O(1)
按值查找O(n)O(n)
// 已知容量时务必预分配,避免反复扩容拷贝
List<String> list = new ArrayList<>(10_000);

1.2 LinkedList:双向链表

底层是 Node<E> 双向链表。每个操作都要遍历定位(index 小于 size/2 从头找,否则从尾找),所以随机访问是 O(n),而头尾插入删除是 O(1)。JDK 中 LinkedList 同时实现 List 和 Deque。

// 何时用 LinkedList?
// 1. 大量头尾操作(它同时是 Deque)
// 2. 需要高效从两端增删的队列场景
// 实际工程:ArrayList 占据 95% 场景,链表应用面其实很窄

一句话总结: ArrayList 是"随机访问利器 + 尾部追加友好",LinkedList 是"两端增删友好 + 随机访问拉胯";绝大多数场景 ArrayList 已足够,别被"链表更快"的直觉误导。


二、HashMap:数组 + 链表 + 红黑树

2.1 数据结构

HashMap 底层:
  数组 table[](桶)
  每个桶可以是:链表(冲突少时)→ 红黑树(冲突≥8 时)
  默认容量 16、负载因子 0.75
  实际使用 hash 的高低位混合:(h ^ (h >>> 16)),减少碰撞
// 扰动函数:让高位参与低位计算,降低碰撞
static final int hash(Object key) {
    int h;
    return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}

2.2 扩容机制

  • 触发条件:size > capacity * loadFactor(默认 16*0.75=12 时扩容)。
  • 扩容策略:翻倍(容量 « 1),并 rehash——但利用"容量是 2 的幂",元素新位置要么不变,要么 = 原位置 + 旧容量,通过位运算快速判断。
  • 树化阈值 8,退化阈值 6:冲突链表长度到 8 且容量≥64 转红黑树;删除后小于 6 退回链表。
// put 流程(伪代码)
final V putVal(int hash, K key, V value, ...) {
    if (table 为空) resize();
    int i = (n - 1) & hash;                 // 定位桶:容量-1 与 hash
    if (桶为空) 直接放入新节点;
    else {
        if (桶头 key 相同) 覆盖;
        else if (桶头是树节点) 走红黑树插入;
        else 遍历链表,尾部插入,若长度 ≥ 8 且容量够则树化;
    }
    if (++size > threshold) resize();
}

2.3 关键参数速记

参数默认含义
初始容量16容量始终取 2 的幂
负载因子0.75时间/空间平衡
树化阈值8链表→红黑树
退化阈值6红黑树→链表(防抖动)
树化最小容量64容量不足 64 先扩容不树化

一句话总结: HashMap = 数组散列 + 冲突解决(链表→红黑树)+ 2 的幂扩容;知道"0.75、8、6、64"这几个魔数,就读懂了它的时间和空间权衡。


三、ConcurrentHashMap:并发容器的正确打开方式

3.1 演进

JDK 版本结构并发控制
JDK7分段锁(Segment)默认 16 个 Segment 各自锁
JDK8+数组 + 链表/红黑树CAS + synchronized 锁桶头

JDK8 放弃分段锁,改用更细粒度的锁单个桶头节点 + CAS,并发度从"16 段"提升到"每桶独立"。

// JDK8 put:桶空 → CAS 直接放入;桶非空 → synchronized 锁桶头再操作
final V putVal(K key, V value, boolean onlyIfAbsent) {
    ...
    for (Node<K,V>[] tab = table;;) {
        if (桶为空 && casTabAt(tab, i, null, node))  // CAS 成功即插入
            break;
        else if (tab[i] == MOVED)  // 扩容中,帮助迁移
            helpTransfer(tab, node);
        else {
            synchronized (tab[i]) {  // 锁桶头
                ... 链表/树插入
            }
        }
    }
}

3.2 正确姿势

  • size 是近似值:高并发下 size() 需要扫描或求和,可能是近似值,别用做精确判断。
  • 不支持 null key/value:避免并发下的二义性。
  • 扩容是并发协作:多线程可"帮助"迁移,减少单线程阻塞。
// 并发累加正确用法:用 LongAdder 而不是并发加计数
ConcurrentHashMap<String, LongAdder> counts = new ConcurrentHashMap<>();
counts.computeIfAbsent("key", k -> new LongAdder()).increment();

一句话总结: JDK8 的 ConcurrentHashMap = “CAS + 锁桶头”,把锁粒度压到单桶;并发读不用锁,并发写只锁冲突桶——这就是它能做到高吞吐的根源。


四、CopyOnWriteArrayList 与并发 Set

4.1 写时复制

CopyOnWriteArrayList:
  读:直接读 volatile 数组引用,无锁
  写:复制整个数组 → 修改 → 替换引用
  适用:读多写极少的场景(如监听器列表、缓存黑名单)
  代价:每次写 O(n) 拷贝;弱一致迭代器
// 经典使用:缓存订阅者列表
private final CopyOnWriteArrayList<Listener> listeners = new CopyOnWriteArrayList<>();

public void addListener(Listener l) { listeners.add(l); }   // 写时复制
public void notifyAll(Event e) {
    for (Listener l : listeners) l.onEvent(e);              // 无锁遍历
}

4.2 并发 Set

  • ConcurrentHashMap.newKeySet() → 用并发 Map 实现的 Set。
  • CopyOnWriteArraySet → 基于 COW List,适合小集合读多写少。
  • Collections.synchronizedSet(new HashSet<>()) → 整表锁,性能差,不推荐高并发。

一句话总结: 写时复制用"空间换读性能"——适合"写极少、读极多"的场景;反之写多就要选分段/锁桶的容器,别把 COW 用成常态。


五、队列家族:Queue/Deque/BlockingQueue

5.1 数据结构与实现

接口代表实现特点
QueueLinkedList、ArrayDequeFIFO 基础队列
DequeArrayDeque、LinkedList双端,头尾都能操作
BlockingQueueArrayBlockingQueue、LinkedBlockingQueue带阻塞语义,用于生产者消费者
并发无锁队列ConcurrentLinkedQueueCAS 实现,无界非阻塞
// 阻塞队列四组 API:失败策略不同
// add/remove/element    → 抛异常
// offer/poll/peek       → 返回 false/null
// put/take              → 永久阻塞
// offer/poll(timeout)   → 限时阻塞
ArrayBlockingQueue<String> q = new ArrayBlockingQueue<>(100);
boolean ok = q.offer("item", 2, TimeUnit.SECONDS);  // 限时入队
String v = q.poll(2, TimeUnit.SECONDS);             // 限时出队

5.2 选型对照

需求推荐
单生产者单消费者ArrayDeque / ArrayBlockingQueue(数组更省空间)
多生产者多消费者、需要容量限制LinkedBlockingQueue 或 ArrayBlockingQueue
无界高吞吐ConcurrentLinkedQueue
延迟执行 / 优先级DelayQueue / PriorityQueue + Blocking
批量 + 并发参考 Disruptor(环形缓冲,非 JDK)

一句话总结: 队列选型先问三件事:有界还是无界?单还是多消费者?要不要阻塞? 三问定了,BlockingQueue 家族的实现自然浮出水面。


六、迭代器与 fail-fast

fail-fast:迭代过程中检测到集合被结构性修改(add/remove/clear),
立即抛 ConcurrentModificationException。
实现:每个集合维护 modCount,迭代器比较 expectedModCount。
List<String> list = new ArrayList<>(List.of("a", "b", "c"));
Iterator<String> it = list.iterator();
it.next();
list.add("d");          // 结构性修改
it.next();              // 抛 ConcurrentModificationException
// 正确删除:用迭代器自己的 remove,而不是集合的 remove
Iterator<String> it = list.iterator();
while (it.hasNext()) {
    if (it.next().equals("a")) it.remove();   // 更新 expectedModCount
}

一句话总结: fail-fast 是"检测到竞争就报错"的快速失败设计,不是并发安全保证;并发遍历该用并发容器的弱一致迭代器或加锁。


七、集合性能与工程选型指南

7.1 使用率最高的选型表

需求首选
有序、可重复、随机访问ArrayList
唯一性HashSet(无序)/ TreeSet(有序)/ LinkedHashSet(插入序)
Key-ValueHashMap / LinkedHashMap / TreeMap
线程安全 MapConcurrentHashMap
线程安全 ListCopyOnWriteArrayList(读多写少)
生产者消费者BlockingQueue 系
先进后出ArrayDeque(不要用 Stack)

7.2 工程建议

  1. 预分配容量:已知数量时给 ArrayList/HashMap 指定初始容量,避免多次扩容。
  2. HashMap 容量设为元素数 / 0.75:new HashMap<>(expectedSize * 4 / 3 + 1)。
  3. Map 遍历选 entrySet 而非先 keys 再 get(少一次查找)。
  4. 不要用 Hashtable / Vector / Stack:全表同步,历史遗留。
  5. Collections.emptyList() 代替 new ArrayList<>() 返回空时零分配。

一句话总结: 集合选型顺序是:线程安全需求 → 有序性 → 唯一性 → 容量预分配;大部分场景 HashMap + ArrayList + ConcurrentHashMap 三件套即可覆盖。


八、易踩的坑

坑现象对策
迭代时集合增删ConcurrentModificationException迭代器 remove / 并发容器
HashMap 初始容量给 0反复扩容给足预期容量
可变对象作 keyhash 变化导致"找不到"key 用不可变对象
高并发用 HashMap死循环/数据丢失用 ConcurrentHashMap
COW 用在写多场景每次全量拷贝写多改用并发写容器
LinkedList 当常规 List 用随机访问 O(n)默认 ArrayList

九、总结

容器数据结构线程安全典型场景
ArrayList动态数组否随机访问为主
LinkedList双向链表否头尾频繁增删
HashMap数组+链表/红黑树否通用 KV
ConcurrentHashMap数组+链表/红黑树是(桶锁)高并发 KV
CopyOnWriteArrayList写时复制数组是(读无锁)读多写少
BlockingQueue队列+阻塞是生产者消费者

一句话记住:集合框架的核心是两个决策轴——数据结构(数组 vs 链表 vs 树 vs 哈希)与并发策略(不加锁 vs 整表锁 vs 桶锁 vs 写时复制)。把这两轴想清楚,任何集合问题都能在 30 秒内给出结论。

延伸阅读

继续阅读

探索更多技术文章

浏览归档,发现更多关于系统设计、工具链和工程实践的内容。

全部文章 返回首页

「java」更多文章

  1. Java 日志体系与工程实践完整指南
  2. Java 异常处理与防御式编程实战
  3. Java 虚拟机类加载机制与字节码深度解析