You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

HashTable的读操作为什么需要加锁?

问题解答

先明确CopyOnWriteArrayList无锁读的核心前提

CopyOnWriteArrayList能做到读完全不加锁,本质是它的所有写操作都不会改动正在被读的旧数组:增删改操作会先完整拷贝一份当前底层数组,在新数组上完成所有修改后,再原子性地把底层数组的引用指向新数组。整个读过程访问的都是完全不可变的旧数组,不存在任何并发修改的可能,最多就是读到写操作生效前的旧快照,属于符合预期的最终一致性,不会触发任何程序错误。

HashTable读不加锁的问题,远不止读到旧数据

你觉得“读到其他线程修改的旧数据业务可接受”,是把并发问题想简单了——HashTable的写操作是直接在原有存储结构上做修改,完全没有写时复制的保护,读不加锁会触发很多致命错误,根本不是“旧值”那么简单:

  • 首先是扩容时的结构错乱:HashTable触发扩容时,会直接在原数组上做链表节点的迁移、重哈希。如果读线程刚好在迁移过程中访问对应的桶,可能读到还没迁移完成的半截链表,要么遍历到未赋值的节点直接抛空指针,要么在旧版实现里触发环形链表,导致读操作死循环占满CPU。
  • 其次是内存可见性问题:没有锁或者volatile的内存语义托底,读线程可能完全感知不到其他线程对哈希表的修改,比如其他线程已经完成了put操作,读线程可能一直读不到对应值,甚至可能读到对象半初始化的无效数据,这已经不是数据新旧的问题,是数据完全错误。
  • 最后是写操作的非原子性问题:put、remove这类操作本身是多步执行的:先算哈希定位桶位置、再遍历链表匹配节点、最后修改链表指针/节点值,整个过程不是原子的。读线程如果卡在写操作执行到一半的时候访问,拿到的就是中间状态的非法数据。
    你说的“旧值可接受”的场景,只适用于写操作不改动原有读访问的存储结构、值的修改是原子替换的情况,HashTable从实现上就不满足这个前提。

ConcurrentHashMap的设计思路和写时复制有本质区别

ConcurrentHashMap确实实现了写加锁、读不加锁的高性能,但它不是靠写时复制实现的,核心逻辑是细粒度锁+内存可见性保证:

  • 写操作没有全局锁:JDK7版本采用分段锁设计,JDK8之后优化为只对当前操作的桶头节点加synchronized锁,锁粒度极小,不会阻塞其他不相关桶的读写操作。
  • 无锁读的托底机制不是数组复制,而是三层保障:
    • 底层存储数组、链表节点的next指针、节点存储的value值都用volatile修饰,保证所有修改对读线程立即可见,不会出现看不到更新的问题
    • 节点值修改、结构初始化这类操作都用CAS原子操作完成,不会出现半修改的中间状态
    • 扩容时采用多线程分段迁移的逻辑,迁移完成的桶会放置转发标记,读操作遇到标记会直接到新数组查询,不会读到迁移过程中的无效数据
      和CopyOnWriteArrayList读固定旧快照的语义不同,ConcurrentHashMap的读可以实时感知到已经完成的写操作,性能远高于全表加锁的HashTable。

内容的提问来源于stack exchange,提问作者dx3906

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.29 15:06:22