ConcurrentHashMap!你居然不知道1.7和1.8可不一样?!

参考文章

ConcurrentHashMap底层实现原理(JDK1.7 & 1.8)

HashMap?ConcurrentHashMap?相信看完这篇没人能难住你!

0. 先上图

对比ConcurrentHashMap的底层数据结构,在JDK1.7和1.8中的不同

ConcurrentHashMap的底层数据结构,在JDK1.7和1.8中的不同

1. JDK1.7中的ConcurrentHashMap

1.1 底层数据结构

首先是数组+链表,其次是Segment数组+HashEntry,看图

1.2 并发控制

ConcurrentHashMap内部进一步细分了若干个小的HashMap,称之为段(segment),默认为16个段。在put()元素的时候,不需要对曾哥HashMap加锁,而是首先根据hash值得到该元素应该被存在哪个段中,然后对该段加锁,并完成put()操作。对段加锁时用的是重入锁ReentrantLock。get()操作不需要加锁,操作也分两步,先定位到段,再定位到具体的桶位。由于 HashEntry 中的 value 属性是用 volatile 关键词修饰的,保证了内存可见性,所以每次获取时都是最新值。

除了put()和get()方法,size()方法在实际开发中也频繁使用,它的目的是统计ConcurrentHashMap的总元素数量, 肯定要把每个segment内部的元素数量都加起来。为了使得效率高,采用的是先乐观后悲观的策略。即先用无锁的方式对各段的元素数进行求和,如果失败的话,就对没个segment都加锁后进行求和。

2. JDK1.8中的ConcurrentHashMap

2.1 底层数据结构

JDK1.8对ConcurrentHashMap的实现已经摒弃了Segment的概念,而是直接用Node数组+链表+红黑树的数据结构来实现,并发控制使用Synchronized和CAS来操作,整个看起来就像是优化过且线程安全的HashMap。虽然在JDK1.8中还能看到Segment的数据结构,但是已经简化了属性,只是为了兼容旧版本。看图

2.2 并发控制

首先Node中的val和next都使用volatile进行修饰,保证可见性,对这两个变量取值时,每次都能取到最新值。

    static class Node<K,V> implements Map.Entry<K,V> {
        final int hash;
        final K key;
        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;
        }
    }

put()方法,使用​​​synchronized和CAS保证多线程安全,看代码(不要被吓到,只需耐心把里面的注释看完)

    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) {
                // 如果没有冲突,CAS方式插入(无锁)
                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)))) {
                                    // 已存在相同的key,覆盖旧值
                                    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,尝试转为红黑树
                    // 之所以说尝试,是因为在treeifyBin方法中,
                    // 还要判断数组容量是不是大于等于64
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        //统计size,并且检查是否需要扩容
        addCount(1L, binCount);
        return null;
    }

put步骤总结:

  • 如果没有初始化就先调用initTable()方法来进行初始化过程
  • 如果没有hash冲突就直接CAS插入
  • 如果还在进行扩容操作就去帮忙转移元素
  • 如果存在hash冲突,就加锁来保证线程安全,这里有两种情况,一种是链表形式就直接遍历到尾端插入,一种是红黑树就按照红黑树结构插入。
  • 最后一个如果,Hash冲突时会形成Node链表,在链表长度超过8,Node数组超过64时会将链表结构转换为红黑树的结构
  • 最后如果添加成功就调用addCount()方法统计size,并且检查是否需要扩容

get()方法操作流程比较简单

    public V get(Object key) {
        Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
        int h = spread(key.hashCode());
        if ((tab = table) != null && (n = tab.length) > 0 &&
            (e = tabAt(tab, (n - 1) & h)) != null) {
            if ((eh = e.hash) == h) {
                // 首节点命中,直接返回
                if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                    return e.val;
            }
            // hash值为负值表示正在扩容,通过ForwardingNode的find方法来查找
            // 如果找到就返回          
            else if (eh < 0)
                return (p = e.find(h, key)) != null ? p.val : null;
            while ((e = e.next) != null) { //既不是首节点也不是ForwardingNode,那就往下遍历
                if (e.hash == h &&
                    ((ek = e.key) == key || (ek != null && key.equals(ek))))
                    return e.val;
            }
        }
        // 无果而返
        return null;
    }

get步骤总结:

  • 计算hash值,定位到该table索引位置,如果是首节点符合就返回
  • 如果遇到扩容的时候,会调用标志正在扩容节点ForwardingNode的find方法,查找该节点,匹配就返回
  • 以上都不符合的话,就往下遍历节点,匹配就返回,否则最后就返回null

3. 总结

JDK1.7版本,ReentrantLock+Segment+HashEntry;JDK1.8版本,synchronized+CAS+Node+红黑树。其实可以看出JDK1.8版本的ConcurrentHashMap的数据结构已经接近HashMap,相对而言,ConcurrentHashMap只是增加了同步的操作来控制并发。

  1. 1.7和1.8都是进行了所粒度的减小。JDK1.7版本锁的粒度是基于Segment的,Segment包含多个HashEntry,而JDK1.8锁的粒度就是HashEntry(首节点)。
  2. JDK1.8版本的数据结构变得更加简单,使得操作也更加清晰流畅,因为已经使用synchronized来进行同步,所以不需要分段锁的概念,也就不需要Segment这种数据结构了,由于粒度的降低,实现的复杂度也增加了
  3. JDK1.8使用红黑树来优化链表,当链表较长时,搜索的复杂度从O(N)优化到O(log(N));
  4. JDK1.8为什么使用内置锁synchronized来代替重入锁ReentrantLock
  • 因为粒度降低了,在相对而言的低粒度加锁方式,synchronized并不比ReentrantLock差,在粗粒度加锁中ReentrantLock可能通过Condition来控制各个低粒度的边界,更加的灵活,而在低粒度中,Condition的优势就没有了
  • JVM的开发团队从来都没有放弃synchronized,而且基于JVM的synchronized优化空间更大,使用内嵌的关键字比使用API更加自然
  • 在大量的数据操作下,对于JVM的内存压力,基于API的ReentrantLock会开销更多的内存,虽然不是瓶颈,但是也是一个选择依据

 

评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值