基础集合与并发问题
ArrayList 是线程安全的吗?
不是。ArrayList 没有任何同步机制,多线程并发操作会产生两类问题:
- 数据丢失:两个线程同时
add(),都读到相同 size,各自写入同一位置,其中一个被覆盖 - 数组越界:扩容时 size 和 elementData.length 不一致,可能抛
ArrayIndexOutOfBoundsException
// 线程不安全 — 结果通常小于 2000
List<Integer> list = new ArrayList<>();
// 两个线程各 add 1000 次,最终 list.size() 可能是 1800、1950…
// 三种安全替代方案:
// 1. 加锁(粒度大,性能差)
List<String> sync = Collections.synchronizedList(new ArrayList<>());
// 2. CopyOnWriteArrayList(写时复制,读多写少场景)
List<String> cow = new CopyOnWriteArrayList<>();
// 3. 局部变量 + 最后汇总(无竞争,最快)CopyOnWriteArrayList 每次写都复制整个数组,写性能差但读无锁,适合"读多写极少"的场景,如事件监听器列表。
HashMap 在多线程下有什么问题?
三大问题,严重程度从高到低:
| 问题 | 版本 | 原因 |
|---|---|---|
| 无限循环/CPU 100% | JDK 7 | 并发 resize 时链表形成环,get() 陷入死循环 |
| 数据丢失 | 7 & 8 | 两线程同时 put 到同一 bucket,一个被覆盖 |
| 数据不可见 | 7 & 8 | 没有 volatile,其他线程可能看不到最新值 |
// JDK 8 改用尾插法解决了成环问题,但数据丢失仍存在
// 生产环境绝对不能用 HashMap 做并发容器!
// 正确替代:
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();JDK 7 的 HashMap 并发扩容会死循环,JDK 8 修了死循环但数据不安全问题依然存在,多线程必须用 ConcurrentHashMap。
头插法为什么会成环?
JDK 7 的 HashMap 扩容使用头插法迁移链表节点。并发 resize 时,两个线程各自持有旧链表的引用,头插法会反转链表顺序,最终形成环形链表:






JDK 8 改用尾插法,保持链表原始顺序,不会再形成环。但并发 put 丢数据的问题仍然存在。
ConcurrentHashMap 是干嘛的?
ConcurrentHashMap 是线程安全的高性能哈希表,核心思路是缩小锁的粒度,避免整个 map 加一把大锁。
JDK 7:分段锁(Segment)
把数组分成 16 个 Segment,每个 Segment 独立加锁。最多 16 个线程并发写,互不干扰。
JDK 8:CAS + synchronized
锁粒度缩小到单个桶(Node)。插入空桶用 CAS 无锁,有碰撞才 synchronized 锁住链表头节点。

ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();
// 原子性的复合操作(重要!不要自己 get 再 put)
map.putIfAbsent("key", 1);
map.computeIfAbsent("key", k -> expensiveCompute(k));
map.merge("key", 1, Integer::sum); // 原子累加
size()不保证精确(并发修改中),get()不需要加锁(读操作无锁)。
什么是死锁?怎么避免?
死锁是两个或多个线程互相持有对方需要的锁,导致永久阻塞。
四个必要条件
同时满足才会发生:
- 互斥:资源同时只能被一个线程持有
- 持有并等待:持有锁 A 的同时等待锁 B
- 不可剥夺:锁不能被强制夺走,只能主动释放
- 循环等待:线程 A 等 B,线程 B 等 A,形成环
触发死锁演示

按顺序获锁演示

避免死锁的四种方法
1. 固定加锁顺序(推荐)
所有线程都按相同顺序获取锁,从根源上打破循环等待。最简单最推荐。
// 不管哪个线程,永远先获取 lock1,再获取 lock2
synchronized (lock1) {
synchronized (lock2) {
// 安全操作
}
}2. tryLock 超时
获取锁失败时主动退出并释放已持有的锁,打破"持有并等待"条件。
// ReentrantLock 方案
if (lock1.tryLock(1, TimeUnit.SECONDS)) {
try {
if (lock2.tryLock(1, TimeUnit.SECONDS)) {
try {
// 安全操作
} finally { lock2.unlock(); }
}
} finally { lock1.unlock(); }
}3. 减少锁粒度
缩小 synchronized 块的范围,或用 ConcurrentHashMap 代替手动加锁,减少同时持有多把锁的机会。
4. 死锁检测工具
生产环境用 jstack 命令或 JVM 自带的 ThreadMXBean 检测死锁,输出线程持锁信息。