Java并发编程中的并发容器与线程安全

ConcurrentHashMap 比 Hashtable 更快,因其分段锁(JDK7)或 CAS+synchronized 单节点锁(JDK8+),而 Hashtable 全表同步;CopyOnWriteArrayList 适合读多写少、允许最终一致性的场景;BlockingQueue 中 ArrayBlockingQueue 有界稳定,LinkedBlockingQueue 无界易OOM;ThreadLocal 需及时 remove 避免内存泄漏。

ConcurrentHashMap 为什么比 Hashtable 更快

因为 ConcurrentHashMap 不锁整个表,而是把

桶数组分段加锁(JDK 7)或用 volatile + CAS + synchronized 控制单个 Node(JDK 8+),而 Hashtable 所有方法都用 synchronized 锁住整个实例。

实操建议:

  • 高并发读多写少场景,优先选 ConcurrentHashMapHashtable 已基本淘汰,除非维护老代码
  • JDK 8+ 中,ConcurrentHashMapcomputeIfAbsent 是线程安全的,但注意其参数函数不能有副作用,否则可能被重复执行
  • 不要用 ConcurrentHashMap.keySet().iterator() 做遍历时修改操作——它不抛 ConcurrentModificationException,但行为不可靠,应改用 forEachentrySet().parallelStream()

CopyOnWriteArrayList 适合什么场景

CopyOnWriteArrayList 在写操作(add/remove/set)时复制整个底层数组,读操作完全无锁。这意味着读性能极高、迭代绝对安全,但写操作代价大、内存占用翻倍、且读到的可能是“旧快照”。

实操建议:

  • 适用于读远多于写、且允许读取延迟(最终一致性)的场景,比如监听器列表、配置白名单缓存
  • 避免在循环中频繁调用 add:每次 add 都触发一次数组复制,O(n) 时间 + O(n) 内存分配
  • 它的 size()get(int) 是弱一致性——调用瞬间返回当前快照长度/元素,但无法反映其他线程刚提交的写入

BlockingQueue 的选择:ArrayBlockingQueue vs LinkedBlockingQueue

两者都是线程安全的阻塞队列,但底层结构和性能特征差异明显。

实操建议:

  • ArrayBlockingQueue 是有界、基于数组、单锁(ReentrantLock)控制入队出队,吞吐量稳定,内存占用可控;适合对资源上限敏感的场景(如限流缓冲区)
  • LinkedBlockingQueue 默认无界(容易 OOM),基于链表,入队出队分别用两个独立锁(takeLock / putLock),读写可并行,吞吐更高;若指定容量,才真正具备背压能力
  • 别直接用无参构造的 LinkedBlockingQueue 处理外部不可控数据流——生产者过快会撑爆堆内存

ThreadLocalMap 的内存泄漏风险怎么规避

ThreadLocal 的底层是每个线程持有一个 ThreadLocalMap,其中 key 是弱引用的 ThreadLocal 实例,value 是强引用。如果线程长期运行(如线程池中的线程),而 ThreadLocal 又没被显式 remove(),就会导致 value 无法回收,形成内存泄漏。

实操建议:

  • 在线程复用场景(如 Web 容器、线程池)中,必须在业务逻辑结束前调用 threadLocal.remove(),尤其在 finally 块里
  • 不要把大对象(如 StringBuilder、缓存 Map)长期绑定在 ThreadLocal 中,即使及时 remove,也可能在 GC 前已造成压力
  • 可以考虑用 InheritableThreadLocal 的子类重写 childValue,但要注意父子线程间的数据拷贝开销

并发容器不是银弹——ConcurrentHashMap 不能替代手动同步复杂复合操作,CopyOnWriteArrayList 不适合高频写,BlockingQueue 的边界策略直接影响系统稳定性,而 ThreadLocal 的生命周期必须和线程生命周期对齐。这些细节一旦忽略,问题往往在线上压测或流量高峰时才暴露。