目录
2. ConcurrentHashMap和HashMap区别?
1.hashMap得底层原理?(代补充)
2.ConcurrentHashMap 1.8得底层原理?
2.1 前置知识
2.1.1是什么?
ConcurrentHashMap解决了HashMap得线程不安全问题,如果出现并发读写操作时候,HashMap可能会出现丢失数据,为了保证安全得同时还能保证效率,采用ConcurrentHashMap。
2.1.2结构
ConcurrentHashMap得存储结构和HashMap结构式一样得,都是数组+链表+红黑树
2.1.3为什么使用红黑树
红黑树更多是解决链表长度太长,查询性能o(n)的问题,而红黑树的时间复杂度是o(log n),写入性能会变慢因为会有树的左旋和右旋。维护红黑树的成本会很高,转红黑树概率为0.00000006,概率非常低。
2.1.4初始化参数
数组初始化大小:16
负载因子:0.75 扩容的触发点。ConcurrentHashMap是不允许修改的,HashMap是可以修改的。
为什么负载因子是0.75?
(1)是基于泊松分布概率学决定的
(2)当0.5的情况下,hash冲突概率笑了,但是数组空间利用不足,当为1的情况下,数组空间利用率高了,但是hash冲突概率高了,并且红黑树概率也变高了
(3)0.75更符合二进制位运算
2.1.5链表什么时候转红黑树?
数组长度大于64 and 链表长度大于8;当红黑树长度小于6情况转换链表。
为什么要满足数组长度大于64?因为链表转红黑树是为了提升查询效率,但是当数组长度短的情况下,hash冲突就可能增加,所以当数组没达到这个限制的时候,先进行扩容操作。
什么时候扩容?其实是元素个数,当元素个数>数组大小*加载因子。
2.1.6Node源码分析:
static class Node<K,V> implements Map.Entry<K,V> {
final int hash;
//传入得key
final K key;
//传入得value
volatile V val;
volatile Node<K,V> next;
Node(int hash, K key, V val, Node<K,V> next) {
this.hash = hash;
this.key = key;
this.val = val;
this.next = next;
}
省略部分代码
....
}
通过源码中分析得出,我们在map.put(key,value),在底层实现就是使用Node节点进行存储得,此处注意点就是hash,key使用final进行修饰,为什么hash值和key使用final修饰,而val和next不用final修饰呢?
解答:使用final域确保初始化安全性,初始化安全性,让不可变对象不需要同步就能自由的被访问和共享。
使用volatile修饰来保证某个变量内存的改变对其他线程即时可见。可以配合CAS实现不加锁对并发操作的支持。
ConcurrentHashMap的get操作可以无锁,由于Node的元素val和指针next是使用volatile修饰的,在多线程环境下,A线程修改节点val或者新增节点对B线程都是即时可见的,保证了数据的一致性。
个人看来,key和hash不牵扯到线程安全的问题,因为key如果变了在map中也就使用链表进行存储;为什么value和next需要呢,更多是因为value,next更新场景,保证可见性是防止其他线程修改时候要让其他线程可见。
2.2 ConcurrentHashMap的DCL操作
懒加载,初始化数组的时候使用了DCL保证线程安全。
private final Node<K,V>[] initTable() {
//声明局部变量
Node<K,V>[] tab; int sc;
//数组没有初始化,才会进行while循环
while ((tab = table) == null || tab.length == 0) {
//正在初始化或者扩容 处理并发场景下同时初始化场景。
if ((sc = sizeCtl) < 0)
//线程等待,让出cpu时间片
Thread.yield();
//这里时>=0情况下,进行CAS操作,修改为-1,表示正在初始化
else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
try {
//双重检查的地方
if ((tab = table) == null || tab.length == 0) {
int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
table = tab = nt;
sc = n - (n >>> 2);
}
} finally {
sizeCtl = sc;
}
break;
}
}
return tab;
}
2.3 sizeCtl属性
是控制数组的初始化和扩容的标识
sizeCtl==-1:当前数组正在初始化
sizeCtl<-1 当前数组正在扩容
sizeCtl==0:刚刚new ConcurrentHashMap
sizeCtl>0:代表下次扩容时的阙值
在new ConcurrentHashMap,指定数组初始化的长度
2.4 ConcurrentHashMap的散列算法
基于key的hashCode和数组长度做运算出来的。
tabAt(tab, i = (n - 1) & hash))
上面代码:可以简略成 Node f=table[(n-1)&hash]
可以计算出数组的索引位置上。
hash值又怎么求出来的呢?
int hash = spread(key.hashCode());
return (h ^ (h >>> 16)) & HASH_BITS;
其实将key的hashCode值,为了降低hash冲突,hash散列,使用高16位和低16位进行异或(相等为1,不能为0)操作。
为什么spred还要有一个与运算?
保证线程安全。
static final int MOVED = -1; // hash for forwarding nodes 正在迁移数据
static final int TREEBIN = -2; // hash for roots of trees 当前索引位置为红黑树
static final int RESERVED = -3; // hash for transient reservations 当前索引位置被占用了,但是还没有值
static final int HASH_BITS = 0x7fffffff; // usable bits of normal node hash
2.5 ConcurrentHashMap的线程安全?
只保证写写操作。不保证读写操作的一致性和安全些。
1.要插入到数组上时,基于CAS保证线程安全
2.要挂到链表或者红黑树时,基于synchronized保证线程安全
2.6 计数器和size
记录的当前元素个数,需要保证效率。
synchronized :太重
cas或者Atomic:当并发过大,线程cas失败会重试,以自旋的方式不断浪费cpu资源。
LongAdder,基于分段的形式,不单使用BaseCount,还基于一个CounterCell数组存储
基于CPU内核数决定CounterCell数组的长度。
就是将BaseCount+CounterCell数组的值累加。
private final void addCount(long x, int check) {
CounterCell[] as; long b, s;
if ((as = counterCells) != null ||
!U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
CounterCell a; long v; int m;
boolean uncontended = true;
if (as == null || (m = as.length - 1) < 0 ||
(a = as[ThreadLocalRandom.getProbe() & m]) == null ||
!(uncontended =
U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
fullAddCount(x, uncontended);
return;
}
if (check <= 1)
return;
s = sumCount();
}
if (check >= 0) {
Node<K,V>[] tab, nt; int n, sc;
while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
(n = tab.length) < MAXIMUM_CAPACITY) {
int rs = resizeStamp(n);
if (sc < 0) {
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
s = sumCount();
}
}
}
2.7 put方法分析
1.首先判断key,value是否为空,为空抛出异常
2.计算key hash值,key.hashCode(),利用高16位异或低16位运算并保证是正数
2.判断是否需要初始化,如果需要进行初始化操作。利用DCL+CAS保证初始化线程安全
3.计算出数组的索引值,判断数组索引位置是否为空,为空使用cas进行赋值操作
4.判断当前是否在迁移操作,如果在迁移帮助迁移
5.进行上锁操作,上锁为第一个索引位置上的元素,使用synchronized保证线程安全
6.判断链表情况下,从头开始遍历,如果找到相同的key,进行值替换,否则追加到最后一个位置上
7.判断是否是红黑树,进行红黑树元素替换或者新增
8.判断链表长度是否符合转红黑树条件,符合转红黑树,在这里会有一种情况,当链表长度大于等于8数组长度不满足64的情况下会触发扩容操作。
9.进行计数操作,利用LangAdder保证线程安全,如果数量达到阙值,会进行扩容操作
final V putVal(K key, V value, boolean onlyIfAbsent) {
//1.进行判断key和value是否为空,为空抛出异常
if (key == null || value == null) throw new NullPointerException();
//2.计算hash值,主要通过高16位异或低16位,并且保证hash值为正数
int hash = spread(key.hashCode());
int binCount = 0;
for (Node<K,V>[] tab = table;;) {
Node<K,V> f; int n, i, fh;
//3.判断数组是否需要初始化,如果需要初始化,进行初始化;使用DCL+CAS保证线程安全
if (tab == null || (n = tab.length) == 0)
tab = initTable();
//4.判断数组索引位置是否为空,为空进行赋值操作,使用CAS保证线程安全
else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
if (casTabAt(tab, i, null,
new Node<K,V>(hash, key, value, null)))
break; // no lock when adding to empty bin
}
//5.如果当前正在迁移,当前线程帮助扩容操作,提高扩容性能,主要是通过步长进行计算
else if ((fh = f.hash) == MOVED)
tab = helpTransfer(tab, f);
else {
//6.获取当前数组索引位置,进行上锁操作。锁的是数组索引第一个位置使用的是synchronized。
V oldVal = null;
synchronized (f) {
if (tabAt(tab, i) == f) {
//7.如果是链表,从头查找hash相同,key相同的,如果找到了,进行值替换,否则在最后一个元素插入新数据
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;
}
}
}
//8.如果是红黑树,进行红黑树处理
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;
}
}
}
}
//9.判断是否转红黑树,数组长度大于64and 链表长度大于等于8,在这里有种特殊情况,就是链表长度大于8数组长度不满足64情况下,也会进行扩容操作。
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
//10.将size加1,利用LongAdder原理保证计数器安全,如果元素个数大于阙值,进行扩容操作。
addCount(1L, binCount);
return null;
}
2.8 扩容原理
2.8.1 扩容时机
(1)数据数量达到阙值
(2)链表长度大于等于8and数组长度大于64情况
(3)putAll时,传入的map的长度经过运算大于当前阙值
2.8.2核心属性sizeCtl
sizeCtl<-1:代表正在扩容
高16位:代表当前扩容的标识戳,跟oldTable长度有关。
低16位:代码当前参与扩容的线程个数有关 -1.
sizeCtl>0:下次扩容的阙值或者初始化的长度
2.8.3扩容前的准备
//n代表旧数组的长度 sc代表sizeCtl
//在基于n计算扩容标识戳
int rs = resizeStamp(n);
/**resizeStamp 方法内容 return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
* numberOfLeadingZeros 计算出当前到前面0的个数
1 << (RESIZE_STAMP_BITS - 1) 保证左移16位,符合位是1,为负数
**/
if (sc < 0) {
if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
transferIndex <= 0)
break;
if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
transfer(tab, nt);
}
else if (U.compareAndSwapInt(this, SIZECTL, sc,
(rs << RESIZE_STAMP_SHIFT) + 2))
transfer(tab, null);
计算出来扩容2倍,为了帮助扩容,其他线程也会帮助扩容
3.总结
1.ConcurrentHashMap如何保证线程安全的?
1.初始化数组的时候,使用DCL 双重检查锁,利用CAS的方式
2.使用volatile修饰一些参数,保证线程可见性,有序性。
3.要插入到数组上时,基于CAS保证线程安全
4.要挂到链表或者红黑树时,基于synchronized保证线程安全
5.计数器基于longAdder,BaseCount+CountCell数组
2. ConcurrentHashMap和HashMap区别?
(1)ConcurrentHashMap不允许key和value都为空,hashMap时允许的
ConcurrentHashMap:
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
}
2.hash计算不同
ConcurrentHashMap:
int hash = spread(key.hashCode());
static final int spread(int h) {
return (h ^ (h >>> 16)) & HASH_BITS;
}
HASH_BITS:01111111 11111111 11111111 11111111
与运算:必须保证hash值为正数。
HashMap:
static final int hash(Object key) {
int h;
return (key == null) ? 0 : (h = key.hashCode()) ^ (h >>> 16);
}
3.ConcurrentHashMap线程安全,HashMap线程不安全
参考1.ConcurrentHashMap如何保证线程安全的?