一、ConcurrentHashMap是什么?
在面试的过程面试问完HashMap还会问安全的方法有哪些?HashMap 是线程不安全的集合类,在并发情况下可能由于线程争用导致程序获取不正确的结果。而ConcurrentHashMap 是对 HashMap 的功能增强,使 HashMap 支持高并发下的读写线程安全。看了ConcurrentHashMap源码发现很多方法和代码跟HashMap相似。
只是思路相同,实现不同而已。
二、ConcurrentHashMap方法部分源码
1.put()方法源码
/**
* Maps the specified key to the specified value in this table.
* Neither the key nor the value can be null.
*/
public V put(K key, V value) {
return putVal(key, value, false);
}
final V putVal(K key, V value, boolean onlyIfAbsent) {
// 不允许插入空值或空键
// 允许value空值会导致get方法返回null时有两种情况:
// 1. 找不到对应的key2. 找到了但是value为null;
// 当get方法返回null时无法判断是哪种情况,在并发环境下containsKey方法已不再可靠,
// 需要返回null来表示查询不到数据。允许key空值需要额外的逻辑处理,占用了数组空间,且并没有多大的实用价值。
// HashMap支持键和值为null,但基于以上原因,ConcurrentHashMap是不支持空键值。
if (key == null || value == null) throw new NullPointerException();
// 高低位异或扰动hashcode,和HashMap类似
int hash = spread(key.hashCode());
// bincount表示链表的节点数
int binCount = 0;
// 尝试多种方法循环处理,后续会有很多这种设计
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
// 情况一:如果数组为空则进行初始化
if (tab == null || (n = tab.length) == 0)
tab = initTable();
// 情况二:目标下标对象为null
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
// 重点:采用CAS进行插入
if (casTabAt(tab, i, null,new Node<K,V>(hash, key, value, null)))
break;
}
// 情况三:数组正在扩容,帮忙迁移数据到新的数组
// 同时会新数组,下次循环就是插入到新的数组
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
// 情况四:直接对节点进行加锁,插入数据
// 下面代码很多,但逻辑和HashMap插入数据大同小异
// 因为已经上锁,不涉及并发安全设计
else {
V oldVal = null;
// 同步加锁
synchronized (f) {
// 重复检查一下刚刚获取的对象有没有发生变化
if (tabAt(tab, i) == f) {
// 链表处理情况
if (fh >= 0) {
binCount = 1;
// 循环链表
for (Node<K,V> e = f;; ++binCount) {
K ek;
// 找到相同的则记录旧值
if (e.hash == hash &&
((ek = e.key) == key ||
(ek != null && key.equals(ek)))) {
oldVal = e.val;
// 判断是否需要更新数值
if (!onlyIfAbsent)
e.val = value;
break;
}
Node<K,V> pred = e;
// 若未找到则插在链表尾
if ((e = e.next) == null) {
pred.next = new Node<K,V>(hash, key, value, null);
break;
}
}
}
// 红黑树处理情况
else if (f instanceof TreeBin) {
Node<K,V> p;
binCount = 2;
if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key, value)) != null) {
oldVal = p.val;
if (!onlyIfAbsent)
p.val = value;
}
}
else if (f instanceof ReservationNode)
throw new IllegalStateException("Recursive update");
}
}
// 判断是否需要转化为红黑树,和返回旧数值
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
// 总数+1
addCount(1L, binCount);
return null;
}
从上面的源码可以看出ConcurrentHashMap 不支持使用 null 作为 key 或者 value。
ConcurrentHashMap添加数据时,采取了CAS+synchronize结合策略。首先会判断该节点是否为null,如果为null,尝试使用CAS添加节点;如果添加失败,说明发生了并发冲突,再对节点进行上锁并插入数据。在并发较低的情景下无需加锁,可以显著提高性能。同时只会CAS尝试一次,也不会造成线程长时间等待浪费cpu时间的情况。ConcurrentHashMap的put方法整体流程如下(并不是全部流程):
流程如下:
1.首先会判断数组是否已经初始化,如若未初始化,会先去初始化数组;
2.如果当前要插入的节点为null,尝试使用CAS插入数据;
3.如果不为null,则判断节点hash值是否为-1;-1表示数组正在扩容,会先去协助扩容,再回来继续插入数据。
4.最后会执行上锁,并插入数据,最后判断是否需要返回旧值;
5.如果不是覆盖旧值,需要更新map中的节点数,也就是图中的addCount方法。
注意到源码中有两个关键方法:初始化数组的initTable(),修改map中节点总数的addCount。是put插入的关键方法。
2.get()方法的源码
public V get(Object key) {
Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
int h = spread(key.hashCode());
if ((tab = table) != null && (n = tab.length) > 0 &&
(e = tabAt(tab, (n - 1) & h)) != null) {
if ((eh = e.hash) == h) {
if ((ek = e.key) == key || (ek != null && key.equals(ek)))
return e.val;
}
else if (eh < 0)
return (p = e.find(h, key)) != null ? p.val : null;
while ((e = e.next) != null) {
if (e.hash == h &&
((ek = e.key) == key || (ek != null && key.equals(ek))))
return e.val;
}
}
return null;
}
通过上面的get()方法源码可以看出,首先要计算 hash 值,然后再根据hash 值找到数组对应位置: (n - 1) & h。最后根据该位置处结点性质进行相应找。如果该位置为 null,那么直接返回 null 。如果该位置处的节点刚好就是需要的值,就返回该节点的值。如果该位置节点的 hash 值小于 0,说明容量不足正在扩容,或者是红黑树进行遍历对比查询数据。
三、可能碰到的一些问题
有了解过ConcurrentHashMap吗?它的存储结构是什么样的?
作为Java开发工程师一定要学习和查看ConcurrentHashMap源码数据结构知识点,
ConcurrentHashMap数据结构跟HashMap一样数组+链表+红黑树实现的。
ConcurrentHashMap1.7和1.8的区别
1.7 版本数组+链表,Segment+HashEntry。锁的粒度基于segment,包含多个HashEntry。锁的粒度比较大。
1.8 版本数组+链表+红黑树(Node+CAS+Synchronized),锁的粒度就是node,大大降低了锁的粒度。
1.8版本ConcurrentHashMap使用什么技术来保证线程安全?
采用了Node+CAS+Synchronized锁的方式来保证线程安全
ConcurrentHashMap默认初始容量是多少?
1.8版本中ConcurrentHashMap默认初始容量是16,可以根据业务需要设置容量大小,不设置就是取默认值。
ConCurrentHashmap 的key,value是否可以为null。
都不可以为null ,如果Key或者Value为null会报空指针异常。
ConcurrentHashMap有哪些构造函数?
有五个构造函数,分别是:无参构造函数,可传入容器大小的构造函数,可传入Map的构造函数,可设置阈值和初始容量的构造函数,可设置初始容量和阈值并发级别等五种,具体源码可查看以下部分
//无参构造函数
public ConcurrentHashMap() {
}
//可传初始容器大小的构造函数
public ConcurrentHashMap(int initialCapacity) {
if (initialCapacity < 0)
throw new IllegalArgumentException();
int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
MAXIMUM_CAPACITY :
tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
this.sizeCtl = cap;
}
//可传入map的构造函数
public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
this.sizeCtl = DEFAULT_CAPACITY;
putAll(m);
}
//可设置阈值和初始容量
public ConcurrentHashMap(int initialCapacity, float loadFactor) {
this(initialCapacity, loadFactor, 1);
}
//可设置初始容量和阈值和并发级别
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;
}
ConCurrentHashmap 每次扩容是原来容量的几倍
每次扩容是原来的2倍
在transfer方法里面会创建一个原数组的俩倍的node数组来存放原数据。
ConcurrentHashMap的get方法是否要加锁,为什么?
不需要,get方法采用了unsafe方法来保证线程安全
java1.8中,ConCurrentHashmap是如何计算它的size大小的?
对于size的计算,在put()的时候使用了addCount()方法。