上一篇文章,我们提到过HashMap是 非线程安全的,以下图为例说明.
图一
假设有A,B两个线程,在执行put方法时,是如图所示的步骤 1.数组长度-1 & hash运算计算出数组索引下标 2.判断该索引位置是否为空,如果为空 ,new一个Entry 对象,不为空则判断索引指针所指向的下一个节点是否为空,去形成链表 3.对索引位置赋值.
按图所示,锁乳线程AB并行执行步骤4,两个线程得到的结果都是该索引位置为null,这时执行步骤5 ,线程A先执行,线程B后执行,就会出现key2将key1 覆盖掉的情况,6的位置GETKey的时候就只能拿到key2所对应的值,那么怎么才能顺序执行呢?不错,就是串行,提到串行,首先想到的肯定是加锁,比如,在put方法上 synchronized修饰,就可以解决这种并发问题,这也是HashTable的解决方式
图二
图三
但是再看第一张图,直接在put方法上加锁,相当于锁住了全局,假如线程Aput到索引1的位置 线程Bput到索引2 的位置,那么本身他们是不是不是冲突的?所以Hashtable的处理方式就造成了性能瓶颈,如果对整个方法去加锁,就造成了资源浪费,基于此,JDK1.7的ConcurrentHashMap就引入了分段锁的概念,去优化这个问题.
图四
1.7中ConcurrentHashMap的结构,是有一个segment数组,其中每一个segment对象持有一个Entry数组,如上图所示,每一个segment就是一把锁,当k1,k2同时向s1中的Entry数组中put时,会产生竞争关系,先获取到s1的线程先执行(锁的逻辑一会我们看源码,先理解流程),k1,k2分别向s1,s5时,他们没有竞争关系,就不会产生阻塞,导致性能变差.接下来,因为我安装的JDK是1.8的,所以1.7的源码这里就不贴了,简单说一下,底层是通过一个while(!tryLock)的自旋锁的方式去向node节点添加数据,这个设计很巧妙,一般的锁线程抢不到锁的时候是阻塞的,只能等待.但是这种自旋他可以在那不到锁的时候去做一些其他的判断,1.7中就是在获取锁的代码块里去查询Node[index]所指向的下一节next点的数据进行一些判断,没有浪费线程空转期的时间片.接下来再看下在JDK1.8中 ConcurrentHashMap 的底层实现.下面是put方法的源码,请看源码我添加注释的地方,下面会一一说明
final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
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)
tab = initTable();
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
}
else if ((fh = f.hash) == MOVED)//四
tab = helpTransfer(tab, f);
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;
}
}
}
}
if (binCount != 0) {
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
addCount(1L, binCount);//七
return null;
}
可以看出,1.8的put方法是通过自旋(一)+cas(三) 这种方式去实现锁的,当线程执行put方法时,先通过tabAt(二)判断该索引位置是否为null,如果为null,则通过cas方式去生成一个新的Node对象并赋值给该索引节点.如果存在,走到(四),走到这里代表f一定不为null,判断f的hash值是否等于MOVED(-1),hash等于-1 代表当前正在扩容,如果为true那就让当前线程加入进去帮助扩容,如果不是走到5,这段代码是不是很熟悉?tabAt判断索引位置与f是否相等,如果是自旋进行值覆盖,否则找到指针指向的下一节点生成一个新的Node对象赋值(六),最后说一下binCount,这个参数主要是记录HashMap的Size,用来扩容和转化红黑树用的.
感谢收看,欢迎批评指正.