本文介绍并发Map,有几点说明:
- 其在Java容器中的划分位置见[1]
- 根接口为Map,“键”和“键值”是否允许
NULL
值由具体实现子类决定 - 根接口为Map,它不像Collection接口继承Iterable接口,故本身没有
iterator()
方法获取迭代器,但是通过Set<K> keySet()
,Collection<V> values()
和Set<Map.Entry<K, V>> entrySet()
方法获取到的3个视图的类型分别为Set
,Collection
和Set
,它们属于Collection接口体系,有iterator()
方法获取迭代器(迭代器类型跟具体实现相关),这些间接的迭代器在这里不是主要矛盾,因此本文不作介绍 - 接下来介绍其常见的三个子类:
- Hashtable
- SynchronizedMap
- ConcurrentHashMap
接下来对具体实现子类的介绍,主要基于以下几个维度:
- “键”和“键值”是否允许
NULL
值 - 实现映射表的核心机制,比如“节点数组+哈希”,“树”
- 实现操作的线程安全策略及该种线程安全策略下的并发性能
一、Hashtable
核心原理:
- “键”和“键值”都不允许
NULL
值 - 实现映射表的核心机制:基于“节点数组+哈希”
- 使用
synchronized
锁实现操作的线程安全,故属于同步容器
。锁粒度为“整体”,所有并发操作(比如“put”,“remove”,“get”,“size”等)都要竞争同一把锁,并发性能较差
接下来主要介绍“哈希和碰撞”和“大小”两个问题。需要说明的两点是:1)接下来的时间复杂度分析不考虑获取synchronized锁,如果考虑的话便会不可分析;2)“时间复杂度分析”跟“并发性能”属于两个维度。
1.1、哈希和碰撞
1、哈希算法
- 第一次哈希:键的
hashCode()
方法值 - 第二次哈希:其源代码为
hash & 0x7FFFFFFF
(hash
为第一次哈希的结果值),其含义是:去掉最高位1,避免第三次哈希出现“负数取余操作”情形 - 第三次哈希:本次哈希是为了散列到槽,其计算公式为
p % n
[假定节点数组大小为n,经过前两次哈希的结果值为p]
2、碰撞解决策略
碰撞可通过“链地址法”解决,对应的链可称之为“碰撞冲突链”,假定链表的长度为K,此时增删改查的时间复杂度为O(K)
。
1.2、大小
Hashtable涉及到两类大小:映射记录大小和节点数组大小。
1.2.1、映射记录大小
指Hashtable中映射记录数量,比如“节点数组大小为8,在且只在第3个节点(槽)有一个长度为4的链表(碰撞冲突链),则映射记录大小为4”:
- 对应于实例成员变量
private transient int count
- 对应于实例成员方法
public synchronized int size()
的返回值(不考虑映射记录大小大于Integer.MAX_VALUE
的情形),该方法直接返回成员变量count
的值,因此时间复杂度是O(1)
1.2.2、节点数组大小
指Hashtable中节点数组大小,即成员变量 private transient Entry<?,?>[] table
的大小。
接下来介绍“初始化节点数组大小”和“扩容节点数组大小”,在介绍之前首先作以下几点说明:
- 为简化描述和便于理解,不考虑极值大小的情况,比如
节点数组大小>=MAX_ARRAY_SIZE(Integer.MAX_VALUE - 8)
- 成员变量
loadFactor
的语义是:负载因子,当映射记录大小 >= 节点数组大小*loadFactor
时扩容节点数组 - 成员变量
threshold
的语义是:threshold=节点数组大小*loadFactor
,即当映射记录大小 >= threshold
时扩容节点数组
1.2.2.1、初始化
即时初始化。
1、构造方法
构造方法初始化节点数组,设置成员变量loadFactor
和threshold
的值,具体描述如下表。
构造方法\设置 | 节点数组初始化大小 | loadFactor | threshold |
---|---|---|---|
Hashtable(int initialCapacity, float loadFactor) |
等于传入的initialCapacity |
等于传入的loadFactor |
等于initialCapacity * loadFactor |
Hashtable(int initialCapacity) |
等于传入的initialCapacity |
等于默认的0.75f |
等于initialCapacity * loadFactor |
Hashtable() |
等于默认的11 |
等于默认的0.75f |
等于initialCapacity * loadFactor |
Hashtable(Map<? extends K, ? extends V> t) |
等于max(2*t.size(), 11) |
等于默认的0.75f |
等于initialCapacity * loadFactor |
有一点说明:后续可知,扩容操作十分耗费性能(申请新节点数组内存资源,对原映射记录进行rehash操作),因此在创建Hashtable实例时最好能够根据预估的“映射记录大小”预计算“节点数组的初始化大小”。由于当映射记录大小 >= 节点数组大小*loadFactor
时进行扩容,故为尽量避免扩容,须使得满足节点数组大小 > 映射记录大小/loadFactor
不等式,上述第4个构造方法中的max(2*t.size(),11)
就是在loadFactor
取常见默认值0.75f
情况下符合该不等式的一个等式,我们自己在创建Hashtable实例时也应该使用该等式预计算传入的节点数组初始化大小。
1.2.2.2、扩容
节点数组只有“扩容”,没有“收缩”,扩容条件为:映射记录大小 >= threshold
。
扩容过程描述如下:
- 假定节点数组原大小为
N
,则扩容后大小为M=N*2+1
。成员变量threshold
的值为M * loadFactor
(语义不变) - 对原映射记录进行rehash操作
二、SynchronizedMap
java.util.Collections
类中的内部类。
1、核心原理
- 由所传入的Map实例,决定“键”和“键值”是否允许
NULL
值 - 由所传入的Map实例,确定实现映射表的核心机制
- 使用
synchronized
锁实现操作的线程安全,故属于同步容器
。锁粒度为“整体”,所有并发操作(比如“put”,“remove”,“get”,“size”等)都要竞争同一把锁,并发性能较差
2、构造参数
Map<K,V> m
:SynchronizedMap实例所基于的底层Map实例,注意其跟“初始化元素来源集合”不一样,后者在复制后就不再有关联Object mutex
:传入的锁对象
三、ConcurrentHashMap
详见《HashMap和ConcurrentHashMap》博文中对ConcurrentHashMap的介绍。
参考文献
[1]《Java容器》