前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >ConcurrentHashMap详解,以及各map的相互比较

ConcurrentHashMap详解,以及各map的相互比较

作者头像
名字是乱打的
发布2022-05-13 10:40:58
1850
发布2022-05-13 10:40:58
举报
文章被收录于专栏:软件工程

并发环境下HashMap是不安全的,容易造成并发修改异常或者死锁现象 而Collections下提供的synchionizedMap虽然因为加了synchionized而变得安全,却也因此极大的降低了性能 由于以上情况,就使得ConcurrentHashMap应运而生

如何优化Hashtable?

  • 通过锁细粒度化,将整锁拆解成多个锁进行优化

将整锁拆为十六个子锁,一个线程每次只操作一个锁并不干扰其他线程操作其他锁,理论上性能提高了16倍

ConcurrentHashMap的扩容因子SizeCtl是用volatile修饰的对其他线程可见 ConcurrentHashMap可以通过computreIfAbsent()和computre()构建java本地缓存,降低程序计算量

ConcurrentHashMap:put方法的源码逻辑

1.判断Node[]数组是否初始化,没有则进行初始化操作 2.通过hash定位数组的索引坐标,是否有Node节点,如果没有则使用CAS进行添加(链表的头节点),添加失败则进入下次循环。 3.检查到内部正在扩容,就帮助它一块扩容。(主要做数据标记,辅助迁移等) 4.如果f=null(头节点不为空),则使用synchronized锁住元素(链表/红黑二叉树的头元素)   4.1如果是Node(链表结构)则执行链表的添加操作。   4.2如果是TreeNode(树型结构)则执行树添加操作。 5.判断链表长度已经达到临界值8,当然这个8是默认值,大家也可以去做调整,当节点数超过这个值就需要把链表转换为树结构。

ConcurrentHashMap总结:比起分段锁Segment,锁拆得更细
  • 首先使用无锁操作CAS插入头节点,失败则循环重试
  • 若头节点已存在,则通过synronchized尝试获取头节点的同步锁,再进行操作
代码语言:javascript
复制
public V put(K key, V value) {
   return putVal(key, value, false);
}

/** Implementation for put and putIfAbsent */
//onlyIfAbsent的意思是在put一个KV时,如果K已经存在什么也不做则返回null
//如果不存在则put操作后返回V值
final V putVal(K key, V value, boolean onlyIfAbsent) {
   //ConcurrentHashMap中是不能有空K或空V的
   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;
       //如果table是空的,就去初始化,下一个循环就不是空的了
       if (tab == null || (n = tab.length) == 0)
           tab = initTable();
           //如果没有取到值,即取i位的元素是空的,为什么i取值是(n-1)&hash??
           //这是hash的精华所在,在这里可以先思考一下
           //此时直接到KV包装成Node节点放在i位置即可
       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
       }
       //MOVED,定义为-1。标记原table正在执行扩容任务,可以去帮忙(支持多线程扩容)
       else if ((fh = f.hash) == MOVED)
           tab = helpTransfer(tab, f);
       else {
           //这种情况是,在i的位置找到了一个元素,说明此元素的K与之间的某个K的hash结果是一样的
           //
           V oldVal = null;
           synchronized (f) {//同步锁住第一个元素
               if (tabAt(tab, i) == f) {//为了安全起见,再一次判断
                   if (fh >= 0) {//节点的hash值大于0,说明是一个链表结构
                       binCount = 1;//记录链表的元素个数
                       for (Node<K,V> e = f;; ++binCount) {
                           K ek;
                           //判断给定的key是否与取出的key相同,如果是则替换元素
                           if (e.hash == hash &&
                               ((ek = e.key) == key ||
                                (ek != null && key.equals(ek)))) {
                               oldVal = e.val;
                               if (!onlyIfAbsent)
                                   e.val = value;
                               break;//直接跳出,这是一种思想。在编程时可以减少一些if else判断
                           }
                           //否则就是不相等,那就把此元素放在链表的最后一个元素
                           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加1,可能会触发扩容
   addCount(1L, binCount);
   return null;
}
CAS性能很高,但是我知道synchronized性能可不咋地,为啥jdk1.8升级之后反而多了synchronized?

synchronized之前一直都是重量级的锁,但是后来java官方是对他进行过升级的,他现在采用的是锁升级的方式去做的。 针对 synchronized 获取锁的方式,JVM 使用了锁升级的优化方式,就是先使用偏向锁优先同一线程使用,然后再次获取锁,如果失败,就升级为 CAS 轻量级锁,如果失败就会短暂自旋,防止线程被系统挂起。最后如果一直都失败就升级为重量级锁。 所以是一步步升级上去的,最初也是通过很多轻量级的方式锁定的。

ConcurrentHashMap的get操作又是怎么样子的呢?
  • 根据计算出来的 hashcode 寻址,如果就在桶上那么直接返回值。
  • 如果是红黑树那就按照树的方式获取值。
  • 就不满足那就按照链表的方式遍历获取值。
关于ConcurrentHashMap主要注意的地方
  • 1.size()方法和mappingCount方法的异同,两者计算是否准确?   1.1 size最大返回 int 最大值,但是这个 Map 的长度是有可能超过 int 最大值的 mappingCount 方法的返回值是 long 类型。所以不必限制最大值必须是Integer.MAX_VALUE   1.2 public long mappingCount()返回映射的数量。应该使用此方法而不是size(),因为ConcurrentHashMap可能包含的映射数多于可以表示为int的映射。返回的值是估计值; 如果有并发插入或删除,实际计数可能会有所不同。 size()返回此映射中键 - 值映射的数量。
  • 2.CouncurrentHahsMap的并发扩容问题 CouncurrentHahsMap在添加元素时候会判断是否有线程在扩容,如果有,它会调用一个helpTransfer()方法,在这个方法里会重新确认一下,如果确实扩容,会添加一个sizeCtl开启一个辅助线程帮助扩容.
  • 3我们这里保证了并发的写put操作是安全的,那么get操作没有加锁,他是怎么保证并发读写线程安全的呢??? 答案: get操作可以无锁是由于Node的元素val和指针next都是用volatile修饰的,在多线程环境下线程A修改结点的val或者新增节点的时候是对线程B可见的。
HashMap、Hashtable、ConccurentHashMap三者区别

参考 https://blog.csdn.net/xpsallwell/article/details/88071038 https://blog.csdn.net/qq_27037443/article/details/94441885

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2022-05-13,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • ConcurrentHashMap:put方法的源码逻辑
  • ConcurrentHashMap总结:比起分段锁Segment,锁拆得更细
  • CAS性能很高,但是我知道synchronized性能可不咋地,为啥jdk1.8升级之后反而多了synchronized?
  • ConcurrentHashMap的get操作又是怎么样子的呢?
  • 关于ConcurrentHashMap主要注意的地方
  • HashMap、Hashtable、ConccurentHashMap三者区别
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档