普通集合线程不安全因其未同步,多线程读写会引发ConcurrentModificationException、数据丢失或结构损坏;ConcurrentHashMap通过CAS加锁单桶、无锁读、协作扩容等机制避免锁竞争。

为什么普通集合在多线程下会出错
因为 ArrayList、HashMap、HashSet 这些类没做同步,多个线程同时读写时,可能触发 ConcurrentModificationException,或产生数据丢失、脏读、结构损坏(比如链表成环)。
典型现象:遍历 ArrayList 时另一个线程调用 add(),抛出 ConcurrentModificationException;或者 HashMap 在扩容时多个线程同时操作,导致死循环(JDK 7)或数据覆盖(JDK 8+)。
- 不是“偶尔出错”,而是**未定义行为**——结果不可预测,且难以复现
-
Vector和Hashtable虽线程安全,但用的是全表锁,吞吐量极低,不适合高并发场景 - 手动加
synchronized锁粒度难控制:锁太粗影响性能,锁太细容易漏掉临界区
ConcurrentHashMap 是怎么避免锁竞争的
JDK 8 的 ConcurrentHashMap 放弃了分段锁(Segment),改用「CAS + synchronized 锁单个桶(bin)」策略,写操作只锁定哈希表中实际发生修改的那个链表头或红黑树根节点。
- 读操作完全无锁,依赖
volatile变量和Unsafe的有序读取保证可见性 - 扩容时支持多线程协作迁移,老表仍可读,新旧表并存过渡,不阻塞读写
-
size()不再是 O(1),而是对每个 bin 的计数求和,可能有短暂误差(最终一致) - 不支持
containsKey()和get()之外的复合操作原子性,比如“检查不存在再 put”得用computeIfAbsent()
哪些并发容器对应哪些使用场景
选错容器会导致功能缺失或性能浪费。关键看是否需要强一致性、是否允许重复、是否要阻塞等待:
立即学习“Java免费学习笔记(深入)”;
- 需要线程安全的「栈」→ 用
ConcurrentLinkedDeque(非阻塞)或ArrayBlockingQueue(阻塞,但得自己模拟栈逻辑);别用Stack或同步包装的LinkedList - 生产者-消费者模型 → 优先选
ArrayBlockingQueue(有界、可重入锁)或LinkedBlockingQueue(无界、双锁分离);高吞吐低延迟场景考虑Disruptor(非 JDK) - 需要线程安全的「计数器」→ 直接用
AtomicInteger,别用ConcurrentHashMap存一堆计数器,除非 key 动态可变 - 读远多于写的配置缓存 →
CopyOnWriteArrayList合适;但千万别在频繁修改的列表上用它,每次写都复制整个数组
常见误用和隐蔽陷阱
并发容器不是银弹,很多问题出在「以为线程安全,其实只是容器本身安全」:
-
ConcurrentHashMap的keySet()返回的集合不是实时快照,遍历时若其他线程修改,不会抛异常,但可能漏数据或重复 —— 需要强一致性遍历请用new ArrayList(map.keySet())先拷贝 -
ConcurrentLinkedQueue的size()是 O(n),高并发下返回值可能瞬间过期,不能用于条件判断(如if (q.size() > 0) q.poll()) -
CopyOnWriteArrayList的迭代器不支持remove(),调用会抛UnsupportedOperationException;它的set()也是写时复制,不是原地更新 - 所有并发容器都不保证「复合操作」原子性,比如
map.containsKey(k) ? map.get(k) : null依然存在竞态,必须换成map.getOrDefault(k, null)或computeIfPresent()
最常被忽略的一点:并发容器解决的是「容器自身结构与元素引用的线程安全」,不解决元素对象内部状态的线程安全。如果往 ConcurrentHashMap 里放可变对象,还得确保 MyMutableObj 自身是线程安全的,或访问时额外同步。









