ConcurrentHashMap解析

    ConcurrentHashMap是hashMap在多线程安全下的实现,当hashMap要作为类成员变量时都可考虑使用concurrentHashMap来替代HahsMap.

HashMap有哪些操作线程不安全

  1. 如果使用的是无参构造器,那么在put时会对数组进行初始化,这个过程是线程不安全的

  2. java1.7之前resize扩容时多线程会导致元素丢失或死环,1.8之后虽然不存在这个问题,但多线程下每个线程都是重复工作

  3. tab[i]=new Node()操作在多线程下可能会导致元素覆盖

  4. size++操作是非线程安全的,导致size不准确

ConcurrenHashMap是如何解决这些问题的(java1.8版本)

put过程源码分析

final V putVal(K key, V value, boolean onlyIfAbsent) {
if (key == null || value == null) throw new NullPointerException();
获取hash值,这里跟hashmap不同的是多了个HASH_BITS,可能是为了更加散列减少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)
如果使用的无参构造器,那么这里就要先初始化数组,方法里面使用了compareAndSwapInt保证只有一个线程能进行数组初始化,且初始化容量默认是16,其他线程再次进入for循环
tab = initTable();
当hash值对应的数组下标位置没有元素,也就是没有hash冲突时,cas保证同时只有一个线程能tab[i]操作成功,这就避免了可能的元素覆盖,操作成功的线程直接break退出,操作失败的线程再次进入for循环
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;
当tab[i]元素不为null,说明有hash冲突,就用synchronized锁住tab[i]上的元素
synchronized (f) {
双重判断,跟单例原理差不多,怕上锁前已被其他线程修改
if (tabAt(tab, i) == f) {
if (fh >= 0) {
binCount = 1;
遍历hash冲突的链表,并用binCount记录元素个数
for (Node<K,V> e = f;; ++binCount) {
K ek;
判断key是不是一个,两个条件:hash相等且(key地址/值相等或key重写的equals相等),判断成立的话用新值覆盖老值并break
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) {
如果链表个数超过了8个则转换为红黑树
if (binCount >= TREEIFY_THRESHOLD)
treeifyBin(tab, i);
if (oldVal != null)
return oldVal;
break;
}
}
}
这个实际就是size++操作和扩容判断,size++使用的是分段计数方式,对于每个分段使用cas进行+1,扩容过程跟put过程差不多,整个过程跟hashMap扩容原理一样,使用了尾插法,也使用synchronized锁,在tab[i]赋值时也使用了cas
addCount(1L, binCount);
return null;
}

总结(如何解决了HashMap线程不安全问题):

  1. 在初始化数组时使用cas保证只有一个线程能成功并去做初始化工作,其他线程则继续for循环

  2. 在没有hash冲突时tab[i]使用cas进行赋值,保证同时只有一个线程赋值成功,其他线程则在下次for循环时进入hash冲突的代码段

  3. 当有hash冲突时使用synchronized锁住tab[i]位置的元素,而且进行了双检查,保证上锁后元素没有变化,然后在锁里进行链表或红黑树的维护,保证了hash冲突时元素的插入也是线程安全的

  4. 在size++时使用了分段计数方式,线程对单独的cell进行cas操作,这样减小了锁冲突的可能性,在保证线程安全的情况下提高了size++的性能,但在size()方法时需要把base和cell的数都加起来,是一种典型的空间换时间的操作

  5. 在扩容时使用的技巧跟put过程差不多,通过cas和synchronized保证线程安全,跟hashmap一样是尾插法和2倍容量

公众号同名,欢迎关注

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值