-
ConcurrentHashMap
和Hashtable
的区别主要体现在实现线程安全的方式上不同。底层数据结构:
-
ConcurrentHashMap:
- JDK 1.7: 采用分段的数组+链表实现。
- JDK 1.8: 采用的数组结构与
HashMap 1.8
的结构一样,为数组+链表/红黑二叉树。
-
Hashtable 和 JDK 1.8 之前的 HashMap:
- 底层数据结构类似,均采用数组+链表的形式。
- 数组是
HashMap
的主体,链表主要是为了解决哈希冲突而存在的。
-
实现线程安全的方式(重要):
在 JDK 1.7 的时候,ConcurrentHashMap
对整个桶数组进行了分割为段(Segment,分段锁)。每一把锁只锁容器其中一部分数据,多线程访问不同数据段的数据,就不会存在锁竞争,提升并发访问的效率。
到了 JDK 1.8 的时候,ConcurrentHashMap
已经摒弃了 Segment 的概念,而是直接用 Node 数组+链表+红黑树的数据结构来实现,并发控制使用 synchronized 和 CAS 来操作。(JDK 1.6 以后 synchronized 锁做了很多优化)整体看起来就像是优化过的线程安全的 HashMap
,虽然在 JDK 1.8 中还能看到 Segment 的数据结构,但已经退化为属性,仅是为了兼容旧版本。
Hashtable(同一把锁):使用 synchronized 来保证线程安全,效率非常低下。当一个线程访问同步方法时,其他线程也访问同步方法,可能会进入阻塞或轮询状态,如使用 put 添加元素,另一个线程不能使用 put 添加元素,也不能使用 get,竞争会越发激烈从而降低性能。
97.ConcurrentHashMap JDK1.7 实现原理:
-
数据分段:
- 数据被分成多个段(Segment),每个段类似于一个小的哈希表。
- 每个段有一个独立的锁,因此一个段的数据被锁定时,其他段的数据仍可以被并发访问。
-
内部结构:
ConcurrentHashMap
由Segment
数组和HashEntry
数组组成。Segment
继承自ReentrantLock
,因此可以对每个段进行加锁。HashEntry
用于存储键值对数据。
-
并发访问:
- 每个
ConcurrentHashMap
有固定数量的Segment
(默认是 16 个),初始化后数量不能改变。 - 每个
Segment
内部包含多个HashEntry
,每个HashEntry
是一个链表节点。
- 每个
-
操作细节:
- 在操作
HashEntry
数据时,首先需要获取对应Segment
的锁。 - 不同
Segment
可以同时进行写操作,而同一个Segment
内的并发写操作会被阻塞。
- 在操作
98.JDK1.8 ConcurrentHashMap 实现原理:
-
取消 Segment 分段锁:
- JDK 1.8 取消了 Segment 分段锁,改用 Node + CAS(Compare-And-Swap)+ synchronized 来确保并发安全。
-
数据结构:
- 结构与 HashMap 1.8 类似,采用数组 + 链表/红黑二叉树。
- 当链表长度超过阈值(默认8)时,链表转为红黑树,提升查询效率,复杂度从 O(N) 降为 O(log(N))。
-
锁粒度更细:
- synchronized 只锁定当前链表或红黑树的节点,避免大范围锁定,提升并发性能。
- 只要 hash 不冲突,不同节点的操作可以并行,不会互相影响。