什么是ConcurrentHashMap
我们知道,HashMap是线程不安全的。为解决HashMap在高并发环境下不能使用的问题,ConcurrentHashMap诞生了,实际上,我们可以认为ConcurrentHashMap是HashMap的线程安全版本。
为什么不使用HashTable
实际上,HashTable和HashMap的实现原理几乎一样,只是HashTable不允许key和value为null,而且它也是HashMap的线程安全版本。但,它为了线程安全付出的代价太大了!从源码可知,HashTable的get/put所有相关操作都是synchronized的,这相当于给哈希表加了一把大锁。多线程访问时候,只要有一个线程访问或操作该对象,那其他线程只能阻塞,相当于将所有的操作串行化,在竞争激烈的并发场景中性能就会非常差。
ConcurrentHashMap的前世今生
ConcurrentHashMap与HashTable最大的区别,就是ConcurrentHashMap采用局部加锁技术,而HashTable使用了全局加锁技术。在jdk1.7中是对Segment加锁,而在jdk1.8是对每个数组元素加锁。
jdk1.7
在jdk1.7中,ConcurrentHashMap使用了分段锁的方式来确保线程安全。
ConcurrentHashMap中的分段锁称为Segment,它即类似于HashMap的结构,即内部拥有一个Entry数组,数组中的每个元素又是一个链表,同时又是一个ReentrantLock(Segment继承了ReentrantLock)。
ConcurrentHashMap使用分段锁技术,将数据分成一段一段的存储,然后给每一段数据配一把锁,当一个线程占用锁访问其中一个段数据的时候,其他段的数据也能被其他线程访问,能够实现真正的并发访问。
该实现方式的优势为写操作的时候可以只对元素所在的Segment进行加锁即可,不会影响到其他的Segment,但,ConcurrentHashMap的hash过程显然比普通哈希表要长,定位一个元素需要经历两次hash操作,第一次定位到Segment,第二次定位到链表头部。
jdk1.8
我在之前的博客介绍过HashMap的底层数据结构在jdk1.8已经更改为数组+链表+红黑树,ConcurrentHashMap参考了HashMap的实现,其数据结构与HashMap大体相同。但是ConcurrentHashMap的jdk1.8版本相比jdk1.7已经取消了分段锁的数据结构,并根据CAS+Synchronized来实现线程安全。
ConcurrentHashMap的继承关系
public class ConcurrentHashMap<K,V> extends AbstractMap<K,V>
implements ConcurrentMap<K,V>, Serializable {}
ConcurrentHashMap的部分属性
// 表的最大容量
private static final int MAXIMUM_CAPACITY = 1 << 30;
// 默认表的大小
private static final int DEFAULT_CAPACITY = 16;
// 默认并发数
private static final int DEFAULT_CONCURRENCY_LEVEL = 16;
// 装载因子
private static final float LOAD_FACTOR = 0.75f;
// 转化为红黑树的表的最小容量
static final int MIN_TREEIFY_CAPACITY = 64;
// 表
transient volatile Node<K,V>[] table;
// 下一个表
private transient volatile Node<K,V>[] nextTable;
// 对表初始化和扩容控制
private transient volatile int sizeCtl;
我们重点介绍一下sizeCtl,它用于table[]的初始化和扩容操作,不同值的代表状态如下
- -1:table[]正在初始化
- -N:表示有N-1个线程正在进行扩容操作
- 非负值:如果table[]未初始化,则表示table需要初始化的大小,如果初始化完成,则表示table[]扩容的阀值,默认是table[]容量的0.75倍
ConcurrentHashMap的构造函数
无参构造函数
public ConcurrentHashMap() {}
创建一个带有默认初始容量 (16)、加载因子 (0.75) 和 concurrencyLevel (16) 的新的空映射
public ConcurrentHashMap(int initialCapacity,float loadFactor, int concurrencyLevel)
public ConcurrentHashMap(int initialCapacity,
float loadFactor, int concurrencyLevel) {
if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0) // 合法性判断
throw new IllegalArgumentException();
if (initialCapacity < concurrencyLevel) // Use at least as many bins
initialCapacity = concurrencyLevel; // as estimated threads
long size = (long)(1.0 + (long)initialCapacity / loadFactor);
int cap = (size >= (long)MAXIMUM_CAPACITY) ?
MAXIMUM_CAPACITY : tableSizeFor((int)size);
this.sizeCtl = cap;
}
创建一个带有指定初始容量、加载因子和并发级别的新的空映射。注意,在JDK1.8中的并发控制都是针对具体的桶而言,即有多少个桶就可以允许多少个并发数。
put操作
public V put(K key, V value) {
return putVal(key, value, false);
}
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException(); // 键或值为空,抛出异常
// 键的hash值经过计算获得hash值
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) { // 无限循环
Node<K,V> f; int n, i, fh;
if (tab == null || (n = tab.length) == 0) // 表为空或者表的长度为0
// 初始化表
tab = initTable();
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) { // 表不为空并且表的长度大于0,并且该桶不为空
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null))) // 比较并且交换值,如tab的第i项为空则用新生成的node替换
break; // no lock when adding to empty bin
}
else if ((fh = f.hash) == MOVED) // 该结点的hash值为MOVED
// 进行结点的转移(在扩容的过程中)
tab = helpTransfer(tab, f);
else {
V oldVal = null;
synchronized (f) { // 加锁同步
if (tabAt(tab, i) == f) { // 找到table表下标为i的节点
if (fh >= 0) { // 该table表中该结点的hash值大于0
// binCount赋值为1
binCount = 1;
for (Node<K,V> e = f;; ++binCount) { // 无限循环
K ek;
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) { // 结点的hash值相等并且key也相等
// 保存该结点的val值
oldVal = e.val;
if (!onlyIfAbsent) // 进行判断
// 将指定的value保存至结点,即进行了结点值的更新
e.val = value;
break;
}
// 保存当前结点
Node<K,V> pred = e;
if ((e = e.next) == null) { // 当前结点的下一个结点为空,即为最后一个结点
// 新生一个结点并且赋值给next域
pred.next = new Node<K,V>(hash, key,
value, null);
// 退出循环
break;
}
}
}
else if (f instanceof TreeBin) { // 结点为红黑树结点类型
Node<K,V> p;
// binCount赋值为2
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
value)) != null) { // 将hash、key、value放入红黑树
// 保存结点的val
oldVal = p.val;
if (!onlyIfAbsent) // 判断
// 赋值结点value值
p.val = value;
}
}
}
}
if (binCount != 0) { // binCount不为0
if (binCount >= TREEIFY_THRESHOLD) // 如果binCount大于等于转化为红黑树的阈值
// 进行转化
treeifyBin(tab, i);
if (oldVal != null) // 旧值不为空
// 返回旧值
return oldVal;
break;
}
}
}
// 增加binCount的数量
addCount(1L, binCount);
return null;
}
步骤大概如下
- 参数校验
- 若table[]未创建,则初始化
- 当table[i]后面无节点时,直接创建Node(无锁操作)
- 如果当前正在扩容,则帮助扩容并返回最新table[]
- 然后在链表或者红黑树中追加节点
- 最后还回去判断是否到达阀值,如到达变为红黑树结构
我们看到,代码中加锁片段用的是synchronized关键字,而不是像jdk1.7中的ReentrantLock。这一点也说明了,synchronized在新版本的JDK中优化的程度和ReentrantLock差不多了。
我们有必要了解一下initTable这个方法,用于初始化表
private final Node<K,V>[] initTable() {
Node<K,V>[] tab; int sc;
while ((tab = table) == null || tab.length == 0) { // 无限循环
if ((sc = sizeCtl) < 0) // sizeCtl小于0,则进行线程让步等待
Thread.yield(); // lost initialization race; just spin
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { // 比较sizeCtl的值与sc是否相等,相等则用-1替换
try {
if ((tab = table) == null || tab.length == 0) { // table表为空或者大小为0
// sc的值是否大于0,若是,则n为sc,否则,n为默认初始容量
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
@SuppressWarnings("unchecked")
// 新生结点数组
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
// 赋值给table
table = tab = nt;
// sc为n * 3/4
sc = n - (n >>> 2);
}
} finally {
// 设置sizeCtl的值
sizeCtl = sc;
}
break;
}
}
// 返回table表
return tab;
}
get操作
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
// 计算key的hash值
int h = spread(key.hashCode());
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) { // 表不为空并且表的长度大于0并且key所在的桶不为空
if ((eh = e.hash) == h) { // 表中的元素的hash值与key的hash值相等
if ((ek = e.key) == key || (ek != null && key.equals(ek))) // 键相等
// 返回值
return e.val;
}
else if (eh < 0) // 结点hash值小于0
// 在桶(链表/红黑树)中查找
return (p = e.find(h, key)) != null ? p.val : null;
while ((e = e.next) != null) { // 对于结点hash值大于0的情况
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
get函数根据key的hash值来计算在哪个桶中,再遍历桶,查找元素,若找到则返回该结点,否则,返回null。
对ConcurrentHashMap的总结
ConcurrentHashMap是线程安全的,与同样线程安全的HashTable相比,采用了局部加锁技术,不同的桶之间的操作不会相互影响,可以并发执行,效率更高,所以在高并发环境下更推荐ConcurrentHashMap。
参考:高并发编程系列:ConcurrentHashMap的实现原理(JDK1.7和JDK1.8)
ConcurrentHashMap的JDK1.8实现
【JUC】JDK1.8源码分析之ConcurrentHashMap(一)