ConcurrentHashMap原理分析和总结(JDK1.8)

有些话、适合烂在心里 提交于 2020-10-04 22:50:08

HashMap的线程安全版本,可以用来替换HashTable。在hash碰撞过多的情况下会将链表转化成红黑树。1.8版本的ConcurrentHashMap的实现与1.7版本有很大的差别,放弃了段锁的概念,借鉴了HashMap的数据结构:数组+链表+红黑树。ConcurrentHashMap不接受nullkey和nullvalue。

数据结构:
数组+链表+红黑树

并发原理:
cas乐观锁+synchronized锁

加锁对象:
数组每个位置的头节点

方法分析:
put方法:
先根据key的hash值定位桶位置,然后cas操作获取该位置头节点,接着使用synchronized锁锁住头节点,遍历该位置的链表或者红黑树进行插入操作。

稍微具体一点:

1.根据key的hash值定位到桶位置

2.判断if(table==null),先初始化table。

3.判断if(table[i]==null),cas添加元素。成功则跳出循环,失败则进入下一轮for循环。

4.判断是否有其他线程在扩容table,有则帮忙扩容,扩容完成再添加元素。进入真正的put步骤

5.真正的put步骤。桶的位置不为空,遍历该桶的链表或者红黑树,若key已存在,则覆盖;不存在则将key插入到链表或红黑树的尾部。

并发问题:假如put操作时正好有别的线程正在对table数组(map)扩容怎么办?

     答:暂停put操作,先帮助其他线程对map扩容。

源码:

复制代码
final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    //分散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)
        //上面已经分析了初始化过程,初始化完成后继续执行死循环
            tab = initTable();
        //数组的第一个元素为空,则赋值
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
        //这里使用了CAS,避免使用锁。如果CAS失败,说明该节点已经发生改变,
        //可能被其他线程插入了,那么继续执行死循环,在链尾插入。
            if (casTabAt(tab, i, null,
                         new Node<K,V>(hash, key, value, null)))
                //可能的出口一         
                break;                   // no lock when adding to empty bin
        }
        //如果tab正在resize,则帮忙一起执行resize
        //这里监测到的的条件是目标桶被设置成了FORWORD。如果桶没有设置为
        //FORWORD节点,即使正在扩容,该线程也无感知。
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        //执行put操作
        else {
            V oldVal = null;
            //这里请求了synchronized锁。这里要注意,不会出现
            //桶正在resize的过程中执行插入,因为桶resize的时候
            //也请求了synchronized锁。即如果该桶正在resize,这里会发生锁等待
            synchronized (f) {
                    //如果是链表的首个节点
                if (tabAt(tab, i) == f) {
                        //并且是一个用户节点,非Forwarding等节点
                    if (fh >= 0) {
                        binCount = 1;
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            //找到相等的元素更新其value
                            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;
            }
        }
    }
    //见下面的分析
    addCount(1L, binCount);
    return null;
}
复制代码

 

get方法:
根据key的hash值定位,遍历链表或者红黑树,获取节点。

具体一点:

1.根据key的hash值定位到桶位置。

2.map是否初始化,没有初始化则返回null。否则进入3

3.定位到的桶位置是否有头结点,没有返回nul,否则进入4

4.是否有其他线程在扩容,有的话调用find方法查找。所以这里可以看出,扩容操作和get操作不冲突,扩容map的同时可以get操作。

5.若没有其他线程在扩容,则遍历桶对应的链表或者红黑树,使用equals方法进行比较。key相同则返回value,不存在则返回null.

并发问题:假如此时正好有别的线程正在对数组扩容怎么办?

      答:没关系,扩容的时候不会破坏原来的table,遍历任然可以继续,不需要加锁。

源码:

//不用担心get的过程中发生resize,get可能遇到两种情况
//1.桶未resize(无论是没达到阈值还是resize已经开始但是还未处理该桶),遍历链表
//2.在桶的链表遍历的过程中resize,上面的resize分析可以看出并未破坏原tab的桶的节点关系,遍历仍可以继续

复制代码
//不用担心get的过程中发生resize,get可能遇到两种情况
//1.桶未resize(无论是没达到阈值还是resize已经开始但是还未处理该桶),遍历链表
//2.在桶的链表遍历的过程中resize,上面的resize分析可以看出并未破坏原tab的桶的节点关系,遍历仍可以继续
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;
        }
        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.链表转换为红黑树时(链表节点个数达到8个可能会转换为红黑树)。如果转换时map长度小于64则直接扩容一倍,不转化为红黑树。如果此时map长度大于64,则不会扩容,直接进行链表转红黑树的操作。

      2.map中总节点数大于阈值(即大于map长度的0.75倍)时会进行扩容。

如何扩容?

      1.创建一个新的map,是原先map的两倍。注意此过程是单线程创建的

      2.复制旧的map到新的map中。注意此过程是多线程并发完成。(将map按照线程数量平均划分成多个相等区域,每个线程负责一块区域的复制任务)

扩容的具体过程:

      答:

      注:扩容操作是hashmap最复杂难懂的地方,博主也是看了很久才看懂个大概。一两句话真的很难说清楚,建议有时间还是看源码比较好。网上很少有人使用通俗易懂语言来描述扩容的机制。所以这里我尝试用自己的语言做一个简要的概括,描述一下大体的流程,供大家参考,如果觉得不错,可以点个赞,表示对博主的支持,谢谢。

      整体思路:扩容是并发扩容,也就是多个线程共同协作,把旧table中的链表一个个复制到新table中。

      1.给多个线程划分各自负责的区域。分配时是从后向前分配。假设table原先长度是64,有四个线程,则第一个到达的线程负责48-63这块内容的复制,第二个线程负责32-47,第三个负责16-31,第四个负责0-15。

      2.每个线程负责各自区域,复制时是一个个从后向前复制的。如第一个线程先复制下标为63的桶的复制。63复制完了接下来复制62,一直向前,直到完成自己负责区域的所有复制。

      3.完成自己区域的任务之后,还没有结束,这时还会判断一下其他线程负责区域有没有完成所有复制任务,如果没有完成,则可能还会去帮助其它线程复制。比如线程1先完成了,这时它看到线程2才做了一半,这时它会帮助线程2去做剩下一半任务。

      4.那么复制到底是怎么完成的呢?线程之间相互帮忙会导致混乱吗?

      5.首先回答上面第一个问题,我们知道,每个数组的每个桶存放的是一个链表(红黑树也可能,这里只讨论是链表情况)。复制的时候,先将链表拆分成两个链表。拆分的依据是链表中的每个节点的hash值和未扩容前数组长度n进行与运算。运算结果可能为0和1,所以结果为0的组成一个新链表,结果为1的组成一个新链表。为0的链表放在新table的 i 位置,为1的链表放在 新table的 i+n处。扩容后新table是原先table的两倍,即长度是2n。

      6.接着回答上面第二个问题,线程之间相互帮忙不会造成混乱。因为线程已完成复制的位置会标记该位置已完成,其他线程看到标记则会直接跳过。而对于正在执行的复制任务的位置,则会直接锁住该桶,表示这个桶我来负责,其他线程不要插手。这样,就不会有并发问题了。

      7.什么时候结束呢?每个线程参加复制前会将标记位sizeCtl加1,同样退出时会将sizeCtl减1,这样每个线程退出时,只要检查一下sizeCtl是否等于进入前的状态就知道是否全都退出了。最后一个退出的线程,则将就table的地址更新指向新table的地址,这样后面的操作就是新table的操作了。

总结:上面的一字一句都是自己看完源码手敲出来的,为了简单易懂,可能会将一些细节忽略,但是其中最重要的思想都还包含在上面。如果有疑问或者有错误的地方,欢迎在评论区留言。

扩容源码:

复制代码
final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
    Node<K,V>[] nextTab; int sc;
    //nextTab为空时,则说明扩容已经完成
    if (tab != null && (f instanceof ForwardingNode) &&
        (nextTab = ((ForwardingNode<K,V>)f).nextTable) != null) {
        int rs = resizeStamp(tab.length);
        while (nextTab == nextTable && table == tab &&
               (sc = sizeCtl) < 0) {
            if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                sc == rs + MAX_RESIZERS || transferIndex <= 0)
                break;
            if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
                transfer(tab, nextTab);
                break;
            }
        }
        return nextTab;
    }
    return table;
}
//复制元素到nextTab
transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
    int n = tab.length, stride;
    //NCPU为CPU核心数,每个核心均分复制任务,如果均分小于16个
    //那么以16为步长分给处理器:例如0-15号给处理器1,16-32号分给处理器2。处理器3就不用接任务了。
    if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
        stride = MIN_TRANSFER_STRIDE; // subdivide range
     //如果nextTab为空则初始化为原tab的两倍,这里只会时单线程进得来,因为这初始化了nextTab,
     //addcount里面判断了nextTab为空则不执行扩容任务
    if (nextTab == null) {            // initiating
        try {
            @SuppressWarnings("unchecked")
            Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];
            nextTab = nt;
        } catch (Throwable ex) {      // try to cope with OOME
            sizeCtl = Integer.MAX_VALUE;
            return;
        }
        nextTable = nextTab;
        transferIndex = n;
    }
    int nextn = nextTab.length;
    //构造一个forword节点
    ForwardingNode<K,V> fwd = new ForwardingNode<K,V>(nextTab);
    boolean advance = true;
    boolean finishing = false; // to ensure sweep before committing nextTab
    for (int i = 0, bound = 0;;) {
        Node<K,V> f; int fh;
        while (advance) {
            int nextIndex, nextBound;
            if (--i >= bound || finishing)
                advance = false;
            else if ((nextIndex = transferIndex) <= 0) {
                i = -1;
                advance = false;
            }
            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;
                // sizeCtl=nextTab.length*0.75=2*tab.length*0.75=tab.length*1.5!!!
                sizeCtl = (n << 1) - (n >>> 1);
                return;
            }
            //sc - 1表示当前线程完成了扩容任务,sizeCtl的线程数要-1
            if (U.compareAndSwapInt(this, SIZECTL, sc = sizeCtl, sc - 1)) {
                    //还有线程在扩容,就不能设置finish为true
                if ((sc - 2) != resizeStamp(n) << RESIZE_STAMP_SHIFT)
                    return;
                finishing = advance = true;
                i = n; // recheck before commit
            }
        }
        else if ((f = tabAt(tab, i)) == null)
            advance = casTabAt(tab, i, null, fwd);
        else if ((fh = f.hash) == MOVED)
            advance = true; // already processed
        else {
        //这保证了不会出现该桶正在resize又执行put操作的情况
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    Node<K,V> ln, hn;
                    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;
                            //这里尽量少的复制链表节点,从lastrun到链尾的这段链表段,无需复制节点,直接复用
                            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);
                        }
                        setTabAt(nextTab, i, ln);
                        setTabAt(nextTab, i + n, hn);
                        setTabAt(tab, i, fwd);
                        advance = true;
                    }
                    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;
                            }
                        }
                        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;
                    }
                }
            }
        }
    }
}
复制代码

 

initTable方法:

复制代码
private final Node<K,V>[] initTable() {
    Node<K,V>[] tab; int sc;
    //如果table为null或者长度为0, //则一直循环试图初始化table(如果某一时刻别的线程将table初始化好了,那table不为null,//线程就结束while循环)。
    while ((tab = table) == null || tab.length == 0) {
        //如果sizeCtl小于0,
        //即有其他线程正在初始化或者扩容,执行Thread.yield()将当前线程挂起,让出CPU时间,
        //该线程从运行态转成就绪态。
        //如果该线程从就绪态转成运行态了,此时table可能已被别的线程初始化完成,table不为
        //null,该线程结束while循环。
        if ((sc = sizeCtl) < 0)
            Thread.yield(); // lost initialization race; just spin
        //如果此时sizeCtl不小于0,即没有别的线程在做table初始化和扩容操作,
        //那么该线程就会调用Unsafe的CAS操作compareAndSwapInt尝试将sizeCtl的值修改成
        //-1(sizeCtl=-1表示table正在初始化,别的线程如果也进入了initTable方法则会执行
        //Thread.yield()将它的线程挂起 让出CPU时间),
        //如果compareAndSwapInt将sizeCtl=-1设置成功 则进入if里面,否则继续while循环。
        else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
            try {
                //再次确认当前table为null即还未初始化,这个判断不能少。
                if ((tab = table) == null || tab.length == 0) {
                    //如果sc(sizeCtl)大于0,则n=sc,否则n=默认的容量大
                    小16,
                    //这里的sc=sizeCtl=0,即如果在构造函数没有指定容量
                    大小,
                    //否则使用了有参数的构造函数,sc=sizeCtl=指定的容量大小。
                    int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                    @SuppressWarnings("unchecked")
                    //创建指定容量的Node数组(table)。
                    Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                    table = tab = nt;
                    //计算阈值,n - (n >>> 2) = 0.75n当ConcurrentHashMap储存的键值对数量
                    //大于这个阈值,就会发生扩容。
                    //这里的0.75相当于HashMap的默认负载因子,可以发现HashMap、Hashtable如果
                    //使用传入了负载因子的构造函数初始化的话,那么每次扩容,新阈值都是=新容
                    //量 * 负载因子,而ConcurrentHashMap不管使用的哪一种构造函数初始化,
                    //新阈值都是=新容量 * 0.75。
                    sc = n - (n >>> 2);
                }
            } finally {
                sizeCtl = sc;
            }
            break;
        }
    }
    return tab;
}
复制代码

 

 

简单来说就是:

1.多线程使用cas乐观锁竞争tab数组初始化的权力。

2.线程竞争成功,则初始化tab数组。

3.竞争失败的线程则让出cpu(从运行态到就绪态)。等再次得到cpu时,发现tab!=null,即已经有线程初始化tab数组了,则退出即可。

remove方法:

复制代码
public V remove(Object key) {
    return replaceNode(key, null, null);
}
    
final V replaceNode(Object key, V value, Object cv) {
    //计算需要移除的键key的哈希地址。
    int hash = spread(key.hashCode());
    //遍历table。
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        //table为空,或者键key所在的bucket为空,则跳出循环返回。
        if (tab == null || (n = tab.length) == 0 ||
            (f = tabAt(tab, i = (n - 1) & hash)) == null)
            break;
        //如果当前table正在扩容,则调用helpTransfer方法,去协助扩容。
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            boolean validated = false;
            //将键key所在的bucket加锁。
            synchronized (f) {
                if (tabAt(tab, i) == f) {
                    //bucket头节点的哈希地址大于等于0,为链表。
                    if (fh >= 0) {
                        validated = true;
                        //遍历链表。
                        for (Node<K,V> e = f, pred = null;;) {
                            K ek;
                            //找到哈希地址、键key相同的节点,进行移除。
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                V ev = e.val;
                                if (cv == null || cv == ev ||
                                    (ev != null && cv.equals(ev))) {
                                    oldVal = ev;
                                    if (value != null)
                                        e.val = value;
                                    else if (pred != null)
                                        pred.next = e.next;
                                    else
                                        setTabAt(tab, i, e.next);
                                }
                                break;
                            }
                            pred = e;
                            if ((e = e.next) == null)
                                break;
                        }
                    }
                    //如果bucket的头节点小于0,即为红黑树。
                    else if (f instanceof TreeBin) {
                        validated = true;
                        TreeBin<K,V> t = (TreeBin<K,V>)f;
                        TreeNode<K,V> r, p;
                        //找到节点,并且移除。
                        if ((r = t.root) != null &&
                            (p = r.findTreeNode(hash, key, null)) != null) {
                            V pv = p.val;
                            if (cv == null || cv == pv ||
                                (pv != null && cv.equals(pv))) {
                                oldVal = pv;
                                if (value != null)
                                    p.val = value;
                                else if (t.removeTreeNode(p))
                                    setTabAt(tab, i, untreeify(t.first));
                            }
                        }
                    }
                }
            }
            //调用addCount方法,将当前ConcurrentHashMap存储的键值对数量-1。
            if (validated) {
                if (oldVal != null) {
                    if (value == null)
                        addCount(-1L, -1);
                    return oldVal;
                }
                break;
            }
        }
    }
    return null;
}
 
复制代码

 

 

总结:
1.扩容完成后做了什么?
nextTable=null    //新数组的引用置为null

tab=nextTab     //旧数组的引用指向新数组

sizeCtl=0.75n    //扩容阈值重新设置,数组元素个数超过这个阈值就会触发扩容

2.concurrentHashMap中设置为volatile的变量有哪些?
Node,nextTable,baseCount,sizeCtl

3.单线程初始化,多线程扩容
4.什么时候触发扩容?
      1.链表转换为红黑树时(链表节点个数达到8个可能会转换为红黑树),table数组长度小于64。

      2.数组中总节点数大于阈值(数组长度的0.75倍)

5.如何保证初始化nextTable时是单线程的?
所有调用transfer的方法(例如helperTransfer、addCount)几乎都预先判断了nextTab!=null,而nextTab只会在transfer方法中初始化,保证了第一个进来的线程初始化之后其他线程才能进入。

6.get操作时扩容怎么办?
7.put操作扩容时怎么办?
8.如何hash定位?
答:h^(h>>>16)&0x7fffffff,即先将hashCode的高16位和低16位异或运算,这个做目的是为了让hash值更加随机。和0x7fffffff相与运算是为了得到正数,因为负数的hash有特殊用途,如-1表示forwardingNode(上面说的表示该位置正在扩容),-2表示是一颗红黑树。

9.forwardingNode有什么内容?
nextTable   //扩容时执向新table的引用

hash=moved  //moved是常量-1,正在扩容的标记

10.扩容前链表和扩容后链表顺序问题

 

 


语言描述很难解释,直接看图,hn指向最后同一类的第一个节点,hn->6->7,此时ln->null,接着从头开始遍历链表;

第一个节点:由于1的hash&n==1,所以应该放到hn指向的链表,采用头插法。hn->1->6->7

第二个节点:同样,hn->2->1->6->7

第三个节点:hash&n==0,所以应该插入到ln链表,采用头插法,ln->3

.....

最后:

ln->5->3  //复制到新table的i位置处

hn->2->1->6->7   //复制到新table的i+n位置处

可以看到ln中所有元素都是后来一个个插入进来的,所以都是逆序

而hn中6->7是初始赋予的所以顺序,而其1,2是后来插入的,所以逆序。

总结:有部分顺序,有部分逆序。看情况
————————————————
版权声明:本文为CSDN博主「却顾所来径」的原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。
原文链接:https://blog.csdn.net/weixin_42130471/article/details/89813248

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!