图解ConcurrentHashMap底层源码

目录

前言

复习HashMap

ConcurrentHashMap和HashMap和Hashtable三者的区别

hello world代码准备

正文

ConcurrentHashMap源码解读

单线程扩容

多线程扩容

总结

课程推荐(免费)


前言

小伙伴们在面试的时候,可能会被面试官问到你知道线程安全的map集合有哪些吗?有了解过ConcurrentHashMap集合吗?ConcurrentHashMap集合为什么线程安全?保证线程安全为什么不使用Hashtable等一系列的连环考问,所以给大家带来ConcurrentHashMap集合的底层源码解读。

注意:只讲解jdk1.8的HashMap和ConcurrentHashMap

复习HashMap

在jdk1.8的ConcurrentHashMap也是变成跟HashMap一样的数据结构,所以开始之前先复习一下jdk1.8的HashMap。

  1. HashMap没有任何锁机制,所以线程不安全
  2. HashMap底层维护了Node数组+Node链表+红黑树。
  3. HashMap初始化和扩容只能是2的乘方
  4. HashMap负载因子阈值是数组的0.75
  5. HashMap链表尾插法
  6. HashMap是懒加载机制
  7. HashMap单链表大于8,数组长度大于64变成红黑树提高链表的查找速度。
  8. HashMap无序(根据hash值确定数组位置)不重复(重复就是替换)。
  9. HashMap的key和value允许为null
  10. .......

而且jdk1.7到jdk1.8的hash算法也进行了升级,尽量避免hash冲撞。

ConcurrentHashMap和HashMap和Hashtable三者的区别

HashMap:线程不安全

Hashtable:线程安全但是效率低

ConcurrentHashMap:线程安全,相比Hashtable效率高

HashMap在多线程下是线程不安全的map集合,作为老一辈的Hashtable通过synchronized同步锁来保证线程安全,但是为什么如今淘汰被ConcurrentHashMap代替呢?就是因为锁的力度太大了,只要是Hashtable中的方法都加上了synchronized。ConcurrentHashMap同样在jdk1.8中加入了synchronized为什么就效率如此高呢?我们下面开始追寻源码!

hello world代码准备

public class ConcurrentHashMapTest {


    public static void main(String[] args) {

        Teacher t1 = new Teacher("haha1",15);
        Teacher t2 = new Teacher("haha2",15);
        Teacher t3 = new Teacher("haha3",15);
        Teacher t4 = new Teacher("haha4",15);
        Teacher t5 = new Teacher("haha5",15);
        Teacher t6 = new Teacher("haha6",15);
        Teacher t7 = new Teacher("haha7",15);
        Teacher t8 = new Teacher("haha8",15);
        Teacher t9 = new Teacher("haha9",15);
        Teacher t10= new Teacher("haha10",15);
        Teacher t11 = new Teacher("haha11",15);
        Teacher t12 = new Teacher("haha12",15);
        Teacher t13 = new Teacher("haha13",15);
        Teacher t14 = new Teacher("haha14",15);




        ConcurrentHashMap concurrentHashMap = new ConcurrentHashMap();


        concurrentHashMap.put(t1.getName(),t1);
        concurrentHashMap.put(t2.getName(),t2);
        concurrentHashMap.put(t3.getName(),t3);
        concurrentHashMap.put(t4.getName(),t4);
        concurrentHashMap.put(t5.getName(),t5);
        concurrentHashMap.put(t6.getName(),t6);
        concurrentHashMap.put(t7.getName(),t7);
        concurrentHashMap.put(t8.getName(),t8);
        concurrentHashMap.put(t9.getName(),t9);
        concurrentHashMap.put(t10.getName(),t10);
        concurrentHashMap.put(t11.getName(),t11);
        concurrentHashMap.put(t12.getName(),t12);
        concurrentHashMap.put(t13.getName(),t13);
        concurrentHashMap.put(t14.getName(),t14);


        concurrentHashMap.entrySet().stream().forEach(item -> System.out.println(item));



    }

}

class Teacher{
    private String name;

    private int age;

    public Teacher(){

    }
    public Teacher(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public int getAge() {
        return age;
    }

    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Teacher{" +
                "name='" + name + '\'' +
                ", age=" + age +
                '}';
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        Teacher teacher = (Teacher) o;
        return age == teacher.age && Objects.equals(name, teacher.name);
    }

    @Override
    public int hashCode() {
        return Objects.hash(name, age);
    }
}

正文

ConcurrentHashMap源码解读

开始之前先了解一下底层的数据结构

在jdk1.8ConcurrentHashMap的底层结构迎来了一个大改变,跟HashMap的底层接口一样。

ConcurrentHashMap底层数据结构​​​​

ConcurrentHashMap底层是一个懒加载,我们可以看到构造方法,就算是带参构造方法也是将值变成2的乘方然后保存起来并没有初始化。

 因为是懒加载,所以就是我们添加值的时候我们初始化,就给put()方法来上一个断点。

final V putVal(K key, V value, boolean onlyIfAbsent) {

       // 这里跟HashMap不一样,这里不运行key和value存在null值。
        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;
    
           // 判断是否是null或者长度为0,也就是是否需要初始化
            if (tab == null || (n = tab.length) == 0)

               // 初始化的方法,默认长度是16,负载因子的阈值是16*0.75=12,负载因子决定扩容时机
               // 这里初始化的时候,因为会存在并发初始化的情况,所以内部使用unsafe的cas操作保证
               // 初始化的原子性
                tab = initTable();

           // 这里使用hash值在通过算法获取到插入位置,再获取到插入位置是否有值。没值就直接添加。
           // 因为要保证线程安全,所以ConcurrentHashMap内部维护了一个Unsafe使用cas操作保证
           // 原子性,所以多线程的情况下是安全的
           // tabAt()方法取值也是通过unsafe的取值。
            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
            }

           // ForwardingNode节点MOVED为-1,达标已经有线程在移动扩容了,此时我们帮助移动
           // 帮助移动完毕就产生新的Node数组,这里就再进入For循环找位置插入
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);

           // 能进到else中,就代表当前进来的值已经是hash冲撞了
            else {
               // 临时变量
                V oldVal = null;
        
               // 同步锁,但是这个同步锁锁的对象是冲撞的head节点,所以他的锁的力度不大
               // 对于其他数组节点、数组链表、数组红黑树的添加操作不影响。
                synchronized (f) {

                   // double check操作,因为之前获取的可能数组扩容变换位置了。
                    if (tabAt(tab, i) == f) {
                
                       // fh是啥?  没错他就是hash值,从上一个else if判断中获取到的
                       // hash值为正数就代表是非红黑树的添加
                        if (fh >= 0) {
                    
                           // 计数器+1,计数器为了后面的扩容做计数
                            binCount = 1;

                           // 因为进到这里肯定是hash冲撞了,所以需要判断是否是同一个key
                           // 因为map集合是无序不重复,所以相同的key就是替换value值。
                           // 但是也有可能只是hash值冲撞了,但是key值并不相同
                           // 所以就产生了单Node链表,所以这里循环也是在遍历链表
                            for (Node<K,V> e = f;; ++binCount) {
                                
                               // 存储已经存在链表的的key值,并非本次添加的key值。
                                K ek;

                               // 判断是否相等,相等就替换
                               // 其实从这里就可以得出hashcode相等,equals不相等
                               // 但是equals相等他的hashcode必然相等
                                if (e.hash == hash &&
                                    ((ek = e.key) == key ||
                                     (ek != null && key.equals(ek)))) {
                
                                   // 如果是相等了就把旧值赋值给临时变量,并且作为返回值返回
                                    oldVal = e.val;

                                   // 对于concurrentHashMap来说默认是不重复,
                                   // 但是可以通过onlyIfAbsent变量来控制新来的值是否替换
                                   // 是使用之前的还是新的
                                    if (!onlyIfAbsent)
                                        e.val = value;
                                    break;
                                }

                               // 走到这里就达标hashcode冲撞了,但是key值并不相等
                               // 所以这里就判断链表的next节点是否为null,为null就添加
                               // 不为null就进入到下一次循环,下一次循环就继续走上面的代码
                               // 继续判断是否key值相等,不相等又来到这里,直到next节点为null
                               // 大家都知道链表长度为8转红黑树,所以这里又可以得出是大于等于8
                                Node<K,V> pred = e;
                                if ((e = e.next) == null) {
                                    pred.next = new Node<K,V>(hash, key,
                                                              value, null);
                                    break;
                                }
                            }
                        }

                       // 红黑树的添加node节点
                        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;
                            }
                        }
                    }
                }

               // 判断是否从链表转成红黑树结构
               // 条件是单链表大于等于8切数组长度为64,如果数组长度不满足64就扩容
                if (binCount != 0) {
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);

                   // 判断是否key值重复,重复了就返回旧的value值
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }

       // 这里是添加一次计数器的值,并且达到负载因子的阈值就会进行扩容
        addCount(1L, binCount);
         
       // 如果是没有key值重复的情况就是返回null,如果key值重复就是返回旧的value值
        return null;
    }

 逐行的代码介绍。

核心代码都在这里了,大家分析之前可以用记事本之类的工具来记录变量因为“Doug Lea”程序员写的代码虽然简洁优美但是读起来有点费力。

我们来分析为什么线程安全?

首先,Node数组初始化的时候,如果没有控制并发,那么可能就会存在多次初始化的操作。那么我们来看看ConcurrentHashMap如何控制的。给 initTable()方法来上一个断点并且追进去。

    private final Node<K,V>[] initTable() {
        Node<K,V>[] tab; int sc;
        while ((tab = table) == null || tab.length == 0) {
            if ((sc = sizeCtl) < 0)
                Thread.yield(); // lost initialization race; just spin
            else if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                try {
                    if ((tab = table) == null || tab.length == 0) {
                        int n = (sc > 0) ? sc : DEFAULT_CAPACITY;
                        @SuppressWarnings("unchecked")
                        Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                        table = tab = nt;
                        sc = n - (n >>> 2);
                    }
                } finally {
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
    }

yield():会让出当前线程的cpu资源,造成一个上线文的切换,并且进入到EntryList中排队继续尝试等待cpu调度。

sizeCtrl默认值是0因为是int类型,所以进入到else if代码块中进入到cas自旋来改变成-1因为-1的定义是在初始化,并且其他没获取到的就返回false进入到下次while循环了,然后再if就进入到Thread.yield()。

我们看到通过cas自旋抢到资格的线程给Node数组初始化的过程,其实也就是给上默认16的大小,负载因子阈值就是n - (n >>> 2);也就是数组长度的0.75倍。但是还记得ConcurrentHashMap的有参构造方法吗?他定义Node数组的初始大小,所以第一次在sc = sizeCtl进行了赋值,后面的int n = (sc > 0) ? sc : DEFAULT_CAPACITY;三目运算符。

我们继续回到putVal()方法中。

 从整体代码中就看到了一个synchronized代码块,那么synchronized之前的操作就不需要上锁了吗?

我们再思考,synchronized 前都是一些什么操作,刚刚已经说过初始化它是使用cas自旋来保证原子性,那么还有hash值没有碰撞直接添加到Node数组中的操作他就不需要上锁吗?

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
}

所以我们追到casTabAt()方法中。

    static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
                                        Node<K,V> c, Node<K,V> v) {
        return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
    }

没错又是cas来保证原子性,所以说,多个线程来同时竞争到Node数组的同一插槽时通过cas来竞争,没抢到的就返回false,就进入到下次循环,下次进来通过hash算法还是同一个插槽所以就进入到else中的循环来看是否是key值重复或者是hash冲撞添加到链表尾部。

单线程扩容

再思考,我们扩容添加到数组节点或者链表节点或者红黑树节点中呢?那么多线程情况下扩容和重新添加新数组如何保证原子性的呢?

我们hello world代码特意put了十几个对象,我们在直接断点调到第12个添加,并且我们直接进入addCount()方法中,进入到扩容transfer(tab, null)方法;

    private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
        int n = tab.length, stride;
        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];
                nextTab = nt;
            } catch (Throwable ex) {      // try to cope with OOME
                sizeCtl = Integer.MAX_VALUE;
                return;
            }
            nextTable = nextTab;
            transferIndex = n;
        }
        int nextn = nextTab.length;
        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 = (n << 1) - (n >>> 1);
                    return;
                }
                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
                }
            }
            else if ((f = tabAt(tab, i)) == null)
                advance = casTabAt(tab, i, null, fwd);
            else if ((fh = f.hash) == MOVED)
                advance = true; // already processed
            else {
                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;
                                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;
                        }
                    }
                }
            }
        }
    }

我只能说不难,但是特别的复杂,因为变量太多了。

这里我们就不一行一行的看了,我直接告诉你们理论,大家通过理论自己debug追吧。

Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n << 1];

nextTab = nt;

这两行代码证明了扩容是原有的一倍,并且ConcurrentHashMap内部维护了一个扩容时的临时的Node数组,用来把原来Node数组的值迁移过去。如图所示

从原来数组最后一位开始,如果为null,就直接给上ForwardingNode类型的标志节点。如图所示。

如果有值会判断是否是head节点,也就是是否存在链表,存在链表就取到链表最后一位。如果不存在链表就直接通过setTabAt()方法将原有的Node节点转移到新数组中,并且将原数组的Node节点给设置上ForwardingNode类型的标志节点。

注:红黑树部分博主没有去追,想追的同学可以看到transfer方法中红黑树部分的迁移

 大致就是这样的循环来遍历,当遍历完第一个节点以后,回回到最后一个节点再重新循环判断hash码是否是-1,因为ForwardingNode节点的hash值都为-1。检查完以后将nextTable赋值给table,然后将nextTable置为null。这样整个扩容就结束了。

多线程扩容

虽然扩容是结束了,但是我们的初衷不是为了追寻多线程情况下的扩容吗?而这里只是一个单线程的扩容。所以下面是多线程扩容。

else if ((fh = f.hash) == MOVED)
  tab = helpTransfer(tab, f);

首先看到putVal()方法的else if代码块中,判断hash值是否为-1,在前面的单线程中我们可以知道,new出来的ForwardingNode节点的hash码就为-1,所以就达标当前putVal的key插入的位置已经在扩容的迁移过程了,所以当前插入的线程就执行helpTransfer()方法,从方法名就能知道帮助迁移,所以我们又可以得出一个结论就是在多线程情况下是多线程扩容。

我们再看到addCount()方法和helpTransfer()方法

 // helpTransfer方法 
 final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
        Node<K,V>[] nextTab; int sc;
        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;
    }



// addCount()方法
private final void addCount(long x, int check) {
        CounterCell[] as; long b, s;
        if ((as = counterCells) != null ||
            !U.compareAndSwapLong(this, BASECOUNT, b = baseCount, s = b + x)) {
            CounterCell a; long v; int m;
            boolean uncontended = true;
            if (as == null || (m = as.length - 1) < 0 ||
                (a = as[ThreadLocalRandom.getProbe() & m]) == null ||
                !(uncontended =
                  U.compareAndSwapLong(a, CELLVALUE, v = a.value, v + x))) {
                fullAddCount(x, uncontended);
                return;
            }
            if (check <= 1)
                return;
            s = sumCount();
        }
        if (check >= 0) {
            Node<K,V>[] tab, nt; int n, sc;
            while (s >= (long)(sc = sizeCtl) && (tab = table) != null &&
                   (n = tab.length) < MAXIMUM_CAPACITY) {
                int rs = resizeStamp(n);
                if (sc < 0) {
                    if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                        sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                        transferIndex <= 0)
                        break;
                    if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1))
                        transfer(tab, nt);
                }
                else if (U.compareAndSwapInt(this, SIZECTL, sc,
                                             (rs << RESIZE_STAMP_SHIFT) + 2))
                    transfer(tab, null);
                s = sumCount();
            }
        }
    }

大致解读一下,他们有相同的代码就是通过cas操作来改变sizeCtl的值,还记得在Node数组初始化的时候,sizeCtl的用途是记录负载因子的阈值,而在这里的用途就是记录一共有多少个线程一起并发扩容。在addCount()方法中第一个if代码块是记录并发累加或减去计数器的值,当计数器的值达到了阈值就要产生扩容,而增删改都会影响到计数器的值。

我们回到transfer()方法中。

if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE)
   stride = MIN_TRANSFER_STRIDE; // subdivide range

NCPU是当前电脑的cpu核数,所以说如果对于单核cpu来说就是Node数组全部节点的扩容都交给一个线程,因为单核也不存在并发,那么对于多核来说呢就是当前数组长度右移3位,也就是除以8再除以cpu核数如果小于默认的16就当前线程处理的16个节点。

我们再往后走,下面的if (nextTab == null) 判断如果是多线程的情况下并没有加锁肯定是能有多个线程进去的,但是代码块中的内容是扩容的初始化操作,那么能运行多线程访问这块,肯定后面有cas锁来控制。我们接着往后走

我们看到while循环中的代码块

            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 (nextTab == null) 初始化代码中对transferIndex = n;对transferIndex做了一个赋值,n是数组的长度,所以while循环第一次只能进入到最后的else if中。我们看到判断条件

很显然是cas自旋,所以就能明白这里在控制并发了。而且还是对transferIndex变量做的处理,

nextIndex在上一个else if中赋值,就是transferIndex变量值,而transferIndex变量值又是Node数组的长度,而stride默认是16,所以三目运算符走:左边的等式,数组长度减去16,所以说没抢到cas的线程下次cas自旋就是获取的是总数组长度-16位开始处理。并且当nextIndex=16的情况下就是为0。所以从这里就能明白是可以多线程协作迁移。然后通过将nextBound长度赋值给临时变量bound和临时变量i来控制当前抢到cas的线程处理迁移的下标。

我们再往下走。

最后面那个else代码块,就是最长的那个else,我们注意到加了synchronized同步代码块,这个跟putVal()方法的synchronized一样,首先锁的是一个链表或者是红黑树,并没有锁整个Node数组,所以来说效率是非常高的,不会影响到其他线程干活。甚至你在扩容的时候,你还能往其他位置添加Node节点。

我们再由上图继续做思考假如我们只有2个线程,那么最前面4个节点谁来处理呢?

回到我们的while循环

            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通不过,又来到else if中,获取到当前的transferIndex,肯定是大于0的所以继续往下一个else if走,所以这里又分配了任务,通过cas来控制只有一个线程得到了这个迁移任务!

再思考多线程的迁移,怎么判断彻底迁移完成呢?

再我们进入transfer()方法前,我们通过sizeCtl变量来记录一共有多少个线程来并发执行。所以这里肯定是通过sizeCtl变量来决定是否全部线程完活。

         
          if (i < 0 || i >= n || i + n >= nextn) {
                int sc;
                if (finishing) {
                    nextTable = null;
                    table = nextTab;
                    sizeCtl = (n << 1) - (n >>> 1);
                    return;
                }

               // 减去迁移前添加的-2
                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
                }
            }

            
           // 扩容迁移前给sizeCtl加2
            else if (U.compareAndSwapInt(this, SIZECTL, sc,
                               (rs << RESIZE_STAMP_SHIFT) + 2))

单线程的时候说了,某个线程迁移扩容后,会检查原本数组节点是否都已经是ForwardingNode

并且当全部线程检查完以后然后做一个新旧数组的赋值和sizeCtl 的初始化然后就完成了扩容。

总结

对于“Doug Lea”先生的代码,我觉得真的佩服到五体投地,甚至觉得不是人写出来的代码真的太牛了,但是可读性确实很差这点不可否认,反正博主追的时候都是那个记事本来记录变量。

如此的复杂的代码我认为用一篇帖子来讲明白肯定不现实。但是博主真的在用心写想通过画图的方式来让读者读懂,并且我认为这种源码,大家可以先通过帖子和视频知道每步大概在干嘛先知道一个结果,然后debug来追、

课程推荐(免费)

为了读不懂的小伙伴,我特意找到了好理解的视频课程,免费的绝对良心推荐

并非只有ConcurrentHashMap,并发包内容都有讲,特别质量,容易理解icon-default.png?t=M0H8https://www.bilibili.com/video/BV16J411h7Rd?p=277特别的仔细,应付面试没问题,不过建议自己一定要debug哦icon-default.png?t=M0H8https://www.bilibili.com/video/BV17i4y1x71z?from=search&seid=15580606268053490496&spm_id_from=333.337.0.0

最后,写帖不易,希望大家点赞收藏+关注,一直有在分析质量帖和质量视频。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

程序员李哈

创作不易,希望能给与支持

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值