ConcurrentHashMap 源码解析

199 阅读11分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 6 月更文挑战」的第7天,点击查看活动详情

在多线程中,我们可能需要操作普通的集合对象,就会出现线程安全问题,例如HashMap在并发更新的情况下,进行Rehash时会造成元素之间会形成一个循环链表,导致死循环问题。尽管1.8对HashMap进行了优化,降低了死循环出现的概率,但是扩容的时候依然可能出现死循环问题。

这时候我们自然想到使用互斥锁来解决线程安全问题,但是这样锁的力度太大了,很可能会导致系统性能急剧下降.

这时候我们就想到能否细化锁,在集合结构上进行优化,从而实现高并发,所以就有了hashTable->hashMap->SynchronizedHashMap->ConcurrentHashMap这么一个优化的过程,其中ConcurrentHashMap从1.7到1.8又做了很大的改动,进一步优化了性能。我可以先简单讲一讲1.7时ConcurrentHashMap是如何实现扩容的,存在的问题出和优化点,然后引出1.8扩容的处理方案,并且详细说一下扩容、get、remove、put方法,还是说您有什么特别想问的内容呢?

1 使用

//创建容量为8的concurrentHashMap
ConcurrentHashMap<String,String> map = new ConcurrentHashMap<>(8);
//多线程并发操作,线程安全
new Thread(()->map.put("1","one")).start();
new Thread(()->map.put("2","two")).start();

2 构造函数

        public ConcurrentHashMap(int initialCapacity) {
            if (initialCapacity < 0)
                throw new IllegalArgumentException();
            int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
                       MAXIMUM_CAPACITY :
                       tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
            this.sizeCtl = cap;
        }
    
        public ConcurrentHashMap(Map<? extends K, ? extends V> m) {
            this.sizeCtl = DEFAULT_CAPACITY;
            putAll(m);
        }
    
        public ConcurrentHashMap(int initialCapacity, float loadFactor) {
            this(initialCapacity, loadFactor, 1);
        }
    
        public ConcurrentHashMap(int initialCapacity,
                                 float loadFactor, int concurrencyLevel) {
            if (!(loadFactor > 0.0f) || initialCapacity < 0 || concurrencyLevel <= 0)
                throw new IllegalArgumentException();
            if (initialCapacity < concurrencyLevel)   // Use at least as many bins
                initialCapacity = concurrencyLevel;   // as estimated threads
            long size = (long)(1.0 + (long)initialCapacity / loadFactor);
            int cap = (size >= (long)MAXIMUM_CAPACITY) ?
                MAXIMUM_CAPACITY : tableSizeFor((int)size);
            this.sizeCtl = cap;
        }

其中 cap是 Node 数组的长度保持为2的整数次方,tableSizeFor(...) 是根据传入的初始容量,确定出一个合适的数组长度,例如1.5被的初始容量+1,再往上取最接近2的整数次方,作为数组长度 cap 的初始值。sizeCtl 用于控制在初始化或者并发扩容的时候的线程数,初值设为 cap,在初始化会详细介绍。

3 初始化

我们可以发现,ConcurrentHashMap在构造函数中仅仅只是设置了一些参数。初始化发生在插入的时候

当多个线程都往里面添加元素的时候,在进行初始化,这里就存在一个问题,多个线程重复初始化。处理方案就是通过CAS修改sizeCtl

private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
    while ((tab = table) == null || tab.length == 0) {
        //sizeCtl < 0说明另外一个线程执行CAS成功,正在进行初始化,当前线程挂起
        if ((sc = sizeCtl) < 0)
            Thread.yield(); // lost initialization race; just spin
        //说明该线程通过CAS获得了初始化的权利,则使用CAS将sizeCtl设置为-1,。表示本线程正在进行初始化
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) { 
            try {
                if ((tab = table) == null || tab.length == 0) {
                    //默认容量为16
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                    @SuppressWarnings("unchecked")
                    //初始化
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                    table = tab = nt;
                    //sizeCtl并非数组长度,所以初始化之后就不再等于数组长度,而是n-n/4=0.75n,表示下一次扩容的阈值
                    sc = n - (n >>> 2);
                }
            } finally {
                //初始化完成,再将sizeCtl设回去
                sizeCtl = sc;
            }
            break;
        }
    }
    return tab;
}

多线程的竞争是通过对 sizeCtl进行 CAS 操作实现的。如果某个线程成功将 sizeCtl设置为-1,那么就拥有初始化的权利并进行初始化,初始化完成结束后,再把 sizeCtl设置回去;其他线程自旋等待直到数组不为null也就是初始化结束,退出整个函数。

因为初始化的工作量很小,所以此处并没有让其他线程帮助初始化,采用的策略是让其他线程一直等待。

sizeCtl是初始化的核心参数,默认为0,用来控制 table 的初始化和扩容操作

  • 默认值为0,如果在构造函数中有参数传入,该值就是2的幂次方
  • 如果线程获得初始化权限了,就会将该值设为-1,主要是防止其他线程进入,其他线程看见该值<0,表明有线程正在初始化,就自旋等待
  • -N代表有N-1个线程在对HashMap进行并发扩容
  • 最后完成初始化后将sizeCtl设置为0.75n,表示扩容后的阈值

put

核心思想依然是HashMap那一套:根据hash值计算节点插入在table位置,如果该位置为空,则直接插入,否则插入到链表或者红黑树中。

public V put(K key, V value) {
    return putVal(key, value, false);
}
​
/** Implementation for put and putIfAbsent */
final V putVal(K key, V value, boolean onlyIfAbsent) {
    //不允许空值放入
    if (key == null || value == null) throw new NullPointerException();
    //计算哈希值
    int hash = spread(key.hashCode());
    //记录slot中元素的个数
    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为空,则初始化
            tab = initTable();
        //如果节点slot i为空,,说明当前slot没有被放入key-value,那么将值封装成Node对象直接插入slot i中。因为对于空的slot,放入元素是不需要上锁的
        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
        }
        //进入这里说明节点slot i非空,通过MOVED标志查看数组是否正在扩容正在迁移,如果正在迁移,调用helpTransfer帮助完成迁移
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        else {
            //进入这里说明slot i非空并且未进行扩容,就正常将元素插入到slot i中
            //使用synchronized对该节点上锁
            V oldVal = null;
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    //fh>=0说明是链表
                    if (fh >= 0) {
                        binCount = 1;
                        //遍历链表
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            //如果当前节点的hashCode与待放入节点的hashCode相同
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) { //比较equals
                                //保存旧值
                                oldVal = e.val;
                                //如果没有设置onlyIfAbsent,那么覆盖旧值
                                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;
                        //如果返回值不为空,说明红黑树已经包含了key,这时候根据onlyIfAbsent看看能否覆盖原来的值
                        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;
}

put方法啊会进入四个分支:

  1. 判断数组桶是否为空,如果为空,则进行初始化 (分支一)
  2. 数组不为空,就判断slot i是否为空,如果为空,说明该元素是该槽第一个元素,直接将对象封装为Node对象插入slot i中 (分支二)
  3. 如果数组非空,那么通过MOVED标志查看是否正在迁移,如果迁移就调用helpTransfer辅助扩容 (分支三)
  4. 如果未迁移,并且通过fh>=0判断出节点为链表,则遍历链表,通过hash和equals比较每个节点,如果找到对应的节点,就根据onlyIfAbsent看是否替换原值;如果没有对应的节点,直接尾插法插入节点 (分支四)
  5. 如果判断节点为红黑树,也是像第四步一样判断 (分支四)
  6. 如果插入元素后链表长度达到临界值,就调用TreeifyBin吧链表转化为红黑树 (分支四)
  7. addCount做两件事情,第一件事情是size+1并且看看是否需要扩容

扩容

主要做两件事:

  1. 构建一个nextTable,其大小为原来大小的两倍,这个步骤是在单线程环境下完成的
  2. 将原来table里面的内容复制到nextTable中,这个步骤是允许多线程操作的,所以性能得到提升,减少了扩容的时间消耗
 private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
            int n = tab.length, stride;
            // 每核处理的量小于16,则强制赋值16
            if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
                stride = MIN_TRANSFER_STRIDE; // subdivide range
            if (nextTab == null) {            // initiating
                try {
                    @SuppressWarnings("unchecked")
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];        //构建一个nextTable对象,其容量为原来容量的两倍
                    nextTab = nt;
                } catch (Throwable ex) {      // try to cope with OOME
                    sizeCtl = Integer.MAX_VALUE;
                    return;
                }
                nextTable = nextTab;
                transferIndex = n;
            }
            int nextn = nextTab.length;
            // 连接点指针,用于标志位(fwd的hash值为-1,fwd.nextTable=nextTab)
            ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
            // 当advance == true时,表明该节点已经处理过了
            boolean advance = true;
            boolean finishing = false; // to ensure sweep before committing nextTab
            for (int i = 0, bound = 0;;) {
                Node<K,V> f; int fh;
                // 控制 --i ,遍历原hash表中的节点
                while (advance) {
                    int nextIndex, nextBound;
                    if (--i >= bound || finishing)
                        advance = false;
                    else if ((nextIndex = transferIndex) <= 0) {
                        i = -1;
                        advance = false;
                    }
                    // 用CAS计算得到的transferIndex
                    else if (U.compareAndSwapInt
                            (this, TRANSFERINDEX, nextIndex,
                                    nextBound = (nextIndex > stride ?
                                            nextIndex - stride : 0))) {
                        bound = nextBound;
                        i = nextIndex - 1;
                        advance = false;
                    }
                }
                if (i < 0 || i >= n || i + n >= nextn) {
                    int sc;
                    // 已经完成所有节点复制了
                    if (finishing) {
                        nextTable = null;
                        table = nextTab;        // table 指向nextTable
                        sizeCtl = (n << 1) - (n >>> 1);     // sizeCtl阈值为原来的1.5倍
                        return;     // 跳出死循环,
                    }
                    // CAS 更扩容阈值,在这里面sizectl值减一,说明新加入一个线程参与到扩容操作
                    if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                        if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                            return;
                        finishing = advance = true;
                        i = n; // recheck before commit
                    }
                }
                // 遍历的节点为null,则放入到ForwardingNode 指针节点,调用compareAndSwapObjectf
                else if ((f = tabAt(tab, i)) == null)
                    advance = casTabAt(tab, i, null, fwd);
                // f.hash == -1 表示遍历到了ForwardingNode节点,意味着该节点已经处理过了
                // 这里是控制并发扩容的核心
                else if ((fh = f.hash) == MOVED)
                    advance = true; // already processed
                else {
                    // 节点加锁
                    synchronized (f) {
                        // 节点复制工作
                        if (tabAt(tab, i) == f) {
                            Node<K,V> ln, hn;
                            // fh >= 0 ,表示为链表节点
                            if (fh >= 0) {
                                // 构造两个链表  一个是原链表  另一个是原链表的反序排列
                                int runBit = fh & n;
                                Node<K,V> lastRun = f;
                                for (Node<K,V> p = f.next; p != null; p = p.next) {
                                    int b = p.hash & n;
                                    if (b != runBit) {
                                        runBit = b;
                                        lastRun = p;
                                    }
                                }
                                if (runBit == 0) {
                                    ln = lastRun;
                                    hn = null;
                                }
                                else {
                                    hn = lastRun;
                                    ln = null;
                                }
                                for (Node<K,V> p = f; p != lastRun; p = p.next) {
                                    int ph = p.hash; K pk = p.key; V pv = p.val;
                                    if ((ph & n) == 0)
                                        ln = new Node<K,V>(ph, pk, pv, ln);
                                    else
                                        hn = new Node<K,V>(ph, pk, pv, hn);
                                }
                                // 在nextTable i 位置处插上链表,调用本地方法putObjectVolatile
                                setTabAt(nextTab, i, ln);
                                // 在nextTable i + n 位置处插上链表
                                setTabAt(nextTab, i + n, hn);
                                // 在table i 位置处插上ForwardingNode 表示该节点已经处理过了
                                setTabAt(tab, i, fwd);
                                // advance = true 可以执行--i动作,遍历节点
                                advance = true;
                            }
                            // 如果是TreeBin,则按照红黑树进行处理,处理逻辑与上面一致
                            else if (f instanceof TreeBin) {
                                TreeBin<K,V> t = (TreeBin<K,V>)f;
                                TreeNode<K,V> lo = null, loTail = null;
                                TreeNode<K,V> hi = null, hiTail = null;
                                int lc = 0, hc = 0;
                                for (Node<K,V> e = t.first; e != null; e = e.next) {
                                    int h = e.hash;
                                    TreeNode<K,V> p = new TreeNode<K,V>
                                            (h, e.key, e.val, null, null);
                                    if ((h & n) == 0) {
                                        if ((p.prev = loTail) == null)
                                            lo = p;
                                        else
                                            loTail.next = p;
                                        loTail = p;
                                        ++lc;
                                    }
                                    else {
                                        if ((p.prev = hiTail) == null)
                                            hi = p;
                                        else
                                            hiTail.next = p;
                                        hiTail = p;
                                        ++hc;
                                    }
                                }
    
                                // 扩容后树节点个数若<=6,将树转链表
                                ln = (lc <= UNTREEIFY_THRESHOLD) ? untreeify(lo) :
                                        (hc != 0) ? new TreeBin<K,V>(lo) : t;
                                hn = (hc <= UNTREEIFY_THRESHOLD) ? untreeify(hi) :
                                        (lc != 0) ? new TreeBin<K,V>(hi) : t;
                                setTabAt(nextTab, i, ln);
                                setTabAt(nextTab, i + n, hn);
                                setTabAt(tab, i, fwd);
                                advance = true;
                            }
                        }
                    }
                }
            }
        }
    
  1. if (nextTab == null) {            // initiating
    try {
    @SuppressWarnings("unchecked")
    Node<K,V>[] nt = (Node<K,V>[])new Node[n << 1];        //构建一个nextTable对象,其容量为原来容量的两倍
    nextTab = nt;
    } catch (Throwable ex) {      // try to cope with OOME
    sizeCtl = Integer.MAX_VALUE;
    return;
    }
    nextTable = nextTab;
    transferIndex = n;
    }单线程初始化nextTable,其中容量为原来HashMap的两倍

  2. 将节点从table移动到nextTable中

    • 初始化ForwardingNode节点,每当一个槽完成迁移后,就把该节点放在槽中,表示该槽已经完成迁移,并且它指向新的ConcurrentHashMap的引用,这样,就能实现扩容时依然能调用get访问数据
    • for (int i = 0, bound = 0;;) {
      Node<K,V> f; int fh;
      // 控制 --i ,遍历原hash表中的节点
      while (advance) {
      int nextIndex, nextBound;
      if (--i >= bound || finishing)
      advance = false;
      else if ((nextIndex = transferIndex) <= 0) {
      i = -1;
      advance = false;
      }
      // 用CAS计算得到的transferIndex
      else if (U.compareAndSwapInt
      (this, TRANSFERINDEX, nextIndex,
      nextBound = (nextIndex > stride ?
      nextIndex - stride : 0))) {
      bound = nextBound;
      i = nextIndex - 1;
      advance = false;
      }
      }一个for循环,通过CAS设置transferindex属性(代表整个数组扩容的进度),也就是为当前线程分配一个stride,CAS操作成功,线程拿到一个stride的迁移任务,初始化i和bound值,i表示当前处理的槽位序号,bound表示槽位边界,从后往前处理节点;没拿到九月继续while循环自旋,直到transferindex小于等于0,说明扩容结束退出循环
    • // 遍历的节点为null,则放入到ForwardingNode 指针节点,调用compareAndSwapObjectf
      else if ((f = tabAt(tab, i)) == null)
      advance = casTabAt(tab, i, null, fwd);当前槽位没有节点,则在table中的i位置放入fwd,这个过程是采用Unsafe.compareAndSwapObjectf方法实现的,很巧妙的实现了节点的并发移动。
    • // f.hash == -1 表示遍历到了ForwardingNode节点,意味着该节点已经处理过了
      // 这里是控制并发扩容的核心
      else if ((fh = f.hash) == MOVED)
      advance = true; // already processed意味着当前槽位的节点已经被处理过了,直接跳过,处理下一个节点
    • // 节点加锁
      synchronized (f) {
      // 节点复制工作
      if (tabAt(tab, i) == f) {
      Node<K,V> ln, hn;
      // fh >= 0 ,表示为链表节点
      if (fh >= 0) {
      // 构造两个链表 一个是原链表 另一个是原链表的反序排列
      int runBit = fh & n;
      Node<K,V> lastRun = f;
      for (Node<K,V> p = f.next; p != null; p = p.next) {
      int b = p.hash & n;
      if (b != runBit) {
      runBit = b;
      lastRun = p;
      }
      }
      if (runBit == 0) {
      ln = lastRun;
      hn = null;
      }
      else {
      hn = lastRun;
      ln = null;
      }
      for (Node<K,V> p = f; p != lastRun; p = p.next) {
      int ph = p.hash; K pk = p.key; V pv = p.val;
      if ((ph & n) == 0)
      ln = new Node<K,V>(ph, pk, pv, ln);
      else
      hn = new Node<K,V>(ph, pk, pv, hn);
      }
      // 在nextTable i 位置处插上链表,调用本地方法putObjectVolatile
      setTabAt(nextTab, i, ln);
      // 在nextTable i + n 位置处插上链表
      setTabAt(nextTab, i + n, hn);
      // 在table i 位置处插上ForwardingNode 表示该节点已经处理过了
      setTabAt(tab, i, fwd);
      // advance = true 可以执行--i动作,遍历节点
      advance = true;进入这个判断说明需要对它迁移,通过fh >= 0 判断是链表,那么就定义两个变量节点,ln(lowNode)和hn(highNode),分别保存哈希值第X位为0和1的节点。通过hashCode % tab.length(等价于hashCode &(tab.length-1)),这也就是说原先位于i位置的元素,在新的hash表的数组一定处于第i个或者第i+n个位置。然后使用setTabAt方法,将一部分链接到nextTab[i]位置,一部分链接到nextTab[i+n]位置。然后把tab[i]位置指向一个ForWardingNode节点

get

get方法是不用加锁的,是非阻塞的。

其中Node节点是重写过的,设置了volatile关键字修饰,致使它每次获取的都是最新设置的值

public V get(Object key) {
            Node<K,V>[] tab; Node<K,V> e, p; int n, eh; K ek;
            // 计算hash
            int h = spread(key.hashCode());
            //要找的slot非空,如果为空直接返回null
            if ((tab = table) != null && (n = tab.length) > 0 &&
                    (e = tabAt(tab, (n - 1) & h)) != null) {
                // 通过hash值比较第一个节点
                if ((eh = e.hash) == h) {
                    //通过地址和equals方法比较,如果是要找的key,直接返回value
                    if ((ek = e.key) == key || (ek != null && key.equals(ek)))
                        return e.val;
                }
                // 树
                else if (eh < 0)
                    return (p = e.find(h, key)) != null ? p.val : null;
                // 链表,遍历
                while ((e = e.next) != null) {
                    if (e.hash == h &&
                            ((ek = e.key) == key || (ek != null && key.equals(ek))))
                        return e.val;
                }
            }
            return null;
        }
  1. 判断table是否为空,如果为空,直接返回null。
  2. 计算key的hash值,并获取指定table中指定位置的Node节点,通过遍历链表或则树结构找到对应的节点,返回value值。

实际选择

使用诸如Synchronized的互斥锁还是ConcurrentHashMap效率更高的本质就是在问CAS和Synchronized哪个效率高?

这是不一定的,需要看并发量和锁定后那段代码的执行时间。

有时候一个线程就应该用HashMap、LinkedList、ArrayList

高并发并且执行时间短就应该使用ConcurrentHashMap、ConcurrentQueue

而代码执行时间较长,线程并发量不是特别高,此时用Synchronized效率更高

所以要结合不同的情况,结合测试结果合理选择互斥锁还是并发容器,而不能看到HashMap,多线程这两个词无脑选择ConcurrentHashMap

参考

JDK源码

《java并发实现原理》

《深入理解java高并发编程》

www.jianshu.com/p/c0642afe0…

toutiao.io/posts/ku8na…

www.cmsblogs.com/?p=2283