基础-ConcurrentHashMap

1. 介绍

在并发编程中使用HashMap可能导致程序死循环或者数据丢失,而使用线程安全的HashTable效率又非常低下,基于上述两个原因,才有ConcurrentHashMap。

1.1 JDK1.7的ConcurrentHashMap

在JDK1.7中,ConcurrentHashMap采用Segment + HashEntry的方式进行实现,结构如下:

一个 ConcurrentHashMap 里包含一个 Segment 数组。Segment 的结构和HashMap类似,是一种数组和链表结构,一个 Segment 包含一个 HashEntry 数组,每个 HashEntry 是一个链表结构的元素,每 个 Segment 守护着一个HashEntry数组里的元素,当对 HashEntry 数组的数据进行修改时,必须首先 获得对应的 Segment的锁。

  • 该类包含两个静态内部类 HashEntry 和 Segment ;前者用来封装映射表的键值对,后者用来充当锁的角色;

  • Segment是一种可重入的锁 ReentrantLock,每个 Segment 守护一个HashEntry 数组里得元素,当对 HashEntry 数组的数据进行修改时,必须首先获得对应的 Segment 锁;

1.2 JDK1.8的ConcurrentHashMap

在JDK1.8中,放弃了Segment臃肿的设计,取而代之的是采用Node + CAS + Synchronized来保证并发安全进行实现,结构如下:

链表或红黑二叉树首节点用CAS插入,Synchronized只锁定当前链表或红黑二叉树的首节点,这样只要hash不冲突,就不会产生并发。

2. 源码

2.1 JDK1.8的ConcurrentHashMap

final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        //两次hash,减少hash冲突,可以均匀分布 
        int hash = spread(key.hashCode());
        int binCount = 0;
        //对这个table进行迭代 
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            if (tab == null || (n = tab.length) == 0)
                //这里就是上面构造方法没有进行初始化,在这里进行判断,为null就调用initTable进行初始化,属于懒汉模式初始化
                tab = initTable();
            else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
                //如果i位置没有数据,就直接无锁插入 
                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) {
                    //如果以上条件都不满足,那就要进行加锁操作,也就是存在hash冲突,锁住链表或者红黑树的头结点  
                    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)))) {
                                      //这里涉及到相同的key进行put就会覆盖原先的value  
                                    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)
                        //如果链表的长度大于8时就会进行红黑树的转换 
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        //统计size,并且检查是否需要扩容  
        addCount(1L, binCount);
        return null;
    }

3. 实践

4. FAQ

5.1 JAVA8的ConcurrentHashMap为什么放弃了分段锁

  • 加入多个分段锁浪费内存空间。

  • 生产环境中, map 在放入时竞争同一个锁的概率非常小,分段锁反而会造成更新等操作的长时间等待。

  • 为了提高 GC 的效率(为什么呢)

  • 个人而言JDK1.8 HashMap采用的是尾插法,而JDK1.7 采用的是头插法,头插法首节点不固定,无法用Synchronized去锁住对象。一旦用到锁,则尽可能缩小锁的粒度。

5.2 ConcurrentHashMap和Hashtable的区别?

ConcurrentHashMap 结合了 HashMap 和 HashTable 二者的优势。HashMap 没有考虑同步,HashTable 考虑了同步的问题。但是 HashTable 在每次同步执行时都要锁住整个结构。 ConcurrentHashMap 锁的方式是稍微细粒度的。

5.3 CAS是非阻塞算法

5. 参考资料

Java8之后的ConcurrentHashMap, 舍弃分段锁

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值