一、 HashMap
、HashTable
与ConcurrentHashMap
的区别
-
HashMap
是线程不安全的。 -
HashTable
是线程安全的。HashTable
的线程安全是采用在每个方法上面都添加上了synchronized
关键字来修饰,即Hashtable
是针对整个table
的锁定。所有访问HashTable
的线程都必须竞争同一把锁。当一个线程访问 HashTable 的同步方法时,其他线程再访问HashTable
的同步方法时,可能会进入阻塞或者轮询的状态。比如:线程 1 使用put
方法来添加元素,线程2不但不能使用put
方法添加元素,也不能使用get
方法来获取元素,所以锁竞争越激烈,该类的效率越低。 -
ConcurrentHashMap使用分段锁技术, ConcurrentHashMap 是由多个 Segment组成(Segment下包含多个 Node,即键值对),每个Segment都有把锁来实现线程安全,当一个线程占用锁访问其中一段数据的时候,其他段的数据也能被其他线程访问。
二、ConcurrentHashMap
在 JDK1.7 中
2.1 概述
jdk1.7中采用了 segment
+ HashEntry
的方式进行实现。
- 一个ConcurrentHashMap 中包含一个 Segment<K,V>[] segments 数组
- 一个Segment对象中包含一个 HashEntry<K,V>[] table 数组
- 一个HashEntry对象包含
如图所示,1.7的ConcurrentHashMap
是由 Segment + HashEnttry
组成,和HashMap
一样,仍然是数组+链表。
ConcurrencyLevel:默认是16,也就是说ConcurrentHashMap有16个Segments,所以理论上,这个时候,最多可以同时支持16个线程并发写,只要它们的操作分别分布在不同的Segment上。这个值在初始化的时候可以设置为其他值,但是一旦初始化后,它是不可以扩容的。
2.2 成员变量
ConcurrentHashMap 在初始化的时候,计算出 Segment数组的大小 ssize 和每个 Segment 中 HashEntry 数组的大小 cap,并初始化Segment 数组的第一个元素;
ssize
:大小为2的幂次方,默认为16cap
:大小也为2的幂次方,最小值为2,最终结果根据初始化容量initialCapacity
进行计算。
它的核心成员变量
Segment 是 ConcurrentHashMap 的一个内部类,主要的组成结构如下:
首先通过 key 定位到 Segment,之后在对应的Segment中进行具体的put操作。
其中的HashEntry组成:
和HashMap非常相似,唯一的区别就是其中的核心数据如value,以及链表都是volatile修饰的,保证了获取时的可见性。
2.3 并发度(Concurrency Level)
并发度可以理解为程序运行时能够同时更新ConcurrentHashMap
且不产生锁竞争的最大线程数,实际上就是ConcurrentHashMap
中的分段锁的个数,即 Segment[]
的数组长度。ConcurrentHashMap
默认的并发度是16,但用户也可以在构造函数中设置并发度。当用户设置并发度的时候,ConcurrentHashMap
会使用大于等于该值的最小2幂指数作为实际并发度(如果用户设置并发度为17,实际并发度则为32)。
运行时,通过将 key 的高 n 位(n = 32- segmentShift
)和并发度-1 做位与运算定位到所在的Segment
。
2.4 初始化
initialCapacity
:初始容量,这个值指的是整个ConcurrentHashMap
的初始容量,实际操作的时候需要平均分给每个Segment
。loadFactor
:负载因子,Segment数组不可以扩容,所以这个负载因子是给每个Segment内部使用的。
注意: JDK7中除了第一个 Segment之外,剩余的Segments采用的是延迟初始化的机制; 每次put
之前需要检查key
对应的Segment
是否为 null
,如果是则调用 ensureSegment()
以确保对应的 Segment被创建。