【數據結構】12.java源碼關於ConcurrentHashMap

目錄


1.ConcurrentMap的內部結構

 2.ConcurrentMap構造函數

3.元素新增策略
4.元素刪除
5.元素修改和查找
6.特殊操作
7.擴容
8.總結

1.ConcurrentMap內部結構

 

 繼承自abstractMap,實現concurrentMap接口,父類和hashmap相似

在開始之前大家應該都瞭解過concurrentHashmap是通過分段鎖的方式實現多線程安全的

(這裏瞭解到1.8之後似乎也拋棄了分段鎖的概念,我們看看吧)

concurrentHashMap內部存在一個node數據結構用來存放元素

    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;
        }

        @Override
        public final K getKey() {
            return key;
        }

        @Override
        public final V getValue() {
            return val;
        }

        //做疑惑操作獲取結果
        @Override
        public final int hashCode() {
            return key.hashCode() ^ val.hashCode();
        }

        @Override
        public final String toString() {
            return key + "=" + val;
        }

        @Override
        public final V setValue(V value) {
            throw new UnsupportedOperationException();
        }

        @Override
        public final boolean equals(Object o) {
            Object k, v, u;
            Map.Entry<?, ?> e;
            //是Map.Entry的子類,並且key相同,value相同,且都不爲空
            return ((o instanceof Map.Entry) &&
                    (k = (e = (Map.Entry<?, ?>) o).getKey()) != null &&
                    (v = e.getValue()) != null &&
                    (k == key || k.equals(key)) &&
                    (v == (u = val) || v.equals(u)));
        }

        /**
         * Virtualized support for map.get(); overridden in subclasses.
         * 這個是用來尋找某個節點,當做鏈表的方式尋找
         */
        Node<K, V> find(int h, Object k) {
            Node<K, V> e = this;
            if (k != null) {
                do {
                    K ek;
                    boolean ok = e.hash == h && ((ek = e.key) == k || (ek != null && k.equals(ek)));
                    if (ok) {

                        return e;
                    }
                } while ((e = e.next) != null);
            }
            return null;
        }
    }

 

 用來保存樹節點的數據結構

 

static final class TreeNode<K,V> extends Node<K,V> {
        TreeNode<K,V> parent;  // red-black tree links
        TreeNode<K,V> left;
        TreeNode<K,V> right;
        TreeNode<K,V> prev;    // needed to unlink next upon deletion
        boolean red;

        TreeNode(int hash, K key, V val, Node<K,V> next,
                 TreeNode<K,V> parent) {
            super(hash, key, val, next);
            this.parent = parent;
        }

        @Override
        Node<K,V> find(int h, Object k) {
            return findTreeNode(h, k, null);
        }

        /**
         * Returns the TreeNode (or null if not found) for the given key
         * starting at given root.
         * 尋找樹節點,二叉查找樹
         */
        final TreeNode<K,V> findTreeNode(int h, Object k, Class<?> kc) {
            if (k != null) {
                TreeNode<K,V> p = this;
                do  {
                    int ph, dir; K pk; TreeNode<K,V> q;
                    TreeNode<K,V> pl = p.left, pr = p.right;
                    if ((ph = p.hash) > h) {
                        //如果當前的hash散列值大於h,那麼目標就在這個節點左邊
                        p = pl;
                    }
                    else if (ph < h) {
                        //小於就在右邊
                        p = pr;
                    }
                    else if ((pk = p.key) == k || (pk != null && k.equals(pk))) {
                        //恰好相等直接返回
                        return p;
                    }
                    else if (pl == null) {
                        //如果爲空,那麼就走另外一邊
                        p = pr;
                    }
                    else if (pr == null) {

                        p = pl;
                    }
                    else if ((kc != null ||
                            (kc = comparableClassFor(k)) != null) &&
                            (dir = compareComparables(kc, k, pk)) != 0) {

                        p = (dir < 0) ? pl : pr;
                    }
                    else if ((q = pr.findTreeNode(h, k, kc)) != null) {

                        return q;
                    }
                    else {
                        p = pl;
                    }
                } while (p != null);
            }
            return null;
        }
    }

 

 2.ConcurrentMap構造函數

 如果是默認的構造函數就是啥都沒有就是個空的

 

 

public TestConcurrentMap() {
    }

    private static final int tableSizeFor(int c) {
        int n = c - 1;
        n |= n >>> 1;
        n |= n >>> 2;
        n |= n >>> 4;
        n |= n >>> 8;
        n |= n >>> 16;
        return (n < 0) ? 1 : (n >= MAXIMUM_CAPACITY) ? MAXIMUM_CAPACITY : n + 1;
    }

    public TestConcurrentMap(int initialCapacity) {
        if (initialCapacity < 0) {

            throw new IllegalArgumentException();
        }
        int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
                MAXIMUM_CAPACITY :
                tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
        this.sizeCtl = cap;
    }

 這裏的操作就是子啊初始化容量的時候把容量大小控制爲比設置的值要大的最小二次冪!!!

爲什麼呢?因爲>>>是無符號右移動,

1.也就是在右移的時候會吧最高位的1移動到下一位,也就是會形成 0。。。010。。。 的數據和0。。。100。。。那麼一個|操作之後那就是0。。。110。。。

2.然後又移動2位,那就是吧之前合併的2個1再往後合併成1

。。。最終的結果就是0000111111111。。。

因爲int是4個字節,也就是32位,那麼最後一次只需要又移16位,就可以吧剩下的一半全部搞定!!!

 

 

 這裏這樣圖解應該就OK了吧

 

 

 3.元素新增策略

 我們並沒有在容器初始化的時候就構建table的實例化,而是在put操作添加數據的時候纔會進行init初始化實例數據

    /**
     * 初始化table大小
     *
     * @return
     */
    private final Node<K, V>[] initTable() {
        Node<K, V>[] tab;
        int sc;
        //開始循環初始化,當表還是爲空的時候不斷循環
        while ((tab = table) == null || tab.length == 0) {
            if ((sc = sizeCtl) < 0) {
                // 線程跳轉,如果sc的大小小於0,那麼就切換線程,當小於0的時候表示在別的線程在初始化表或擴展表
                Thread.yield();
            }
            //SIZECTL:表示當前對象的內存偏移量,sc表示期望值,-1表示要替換的值,設定爲-1表示要初始化表了
            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大小爲去掉1/4,這個就是類似加載因子0.75,實際可用大小
                        sc = n - (n >>> 2);
                    }
                } finally {
                    //初始化完畢設置成想要的值 初始化後,sizeCtl長度爲數組長度的3/4
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
    }

 

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) {
                if (casTabAt(tab, i, null,
                        new Node<K, V>(hash, key, value, null))) {
                    // no lock when adding to empty bin
                    break;
                }
            } else if ((fh = f.hash) == MOVED) {
                //如果取出來的節點的hash值是MOVED(-1)的話,則表示當前正在對這個數組進行擴容,複製到新的數組,則當前線程也去幫助複製
                tab = helpTransfer(tab, f);
            } else {
                //如果hash值一樣的位置有值了,那麼就需要對指定的槽位f(指定槽位的引用)上鎖
                V oldVal = null;
                synchronized (f) {
                    //再比較一次
                    if (tabAt(tab, i) == f) {
                        //fh 這個位置的hash值如果大於0,說明不是在擴容階段
                        if (fh >= 0) {
                            binCount = 1;
                            for (Node<K, V> e = f; ; ++binCount) {
                                K ek;
                                //判斷hash是否相等,判斷是否在一個槽位,判斷key是否相等,判斷是否一個元素,
                                if (e.hash == hash && ((ek = e.key) == key || (ek != null && key.equals(ek)))) {
                                    oldVal = e.val;
                                    if (!onlyIfAbsent) {
                                        //如果hash和key都相等,那麼直接替換值即可
                                        e.val = value;
                                    }
                                    break;
                                }
                                Node<K, V> pred = e;
                                //指向下一個節點,如果key不等
                                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) {

                        treeifyBin(tab, i);
                    }
                    if (oldVal != null) {

                        return oldVal;
                    }
                    break;
                }
            }
        }
        addCount(1L, binCount);
        return null;
    }

 

 

 對於插入操作,其實和hashmap是一樣的,然後就是多了個擴容不一樣

其他至於紅黑樹的平衡,這個可以參考我之前的博客treeMap應該有詳解-》https://www.cnblogs.com/cutter-point/p/11587453.html

 並且因爲在插入的時候有對當前位置上鎖,所以考慮到了多線程的問題,並且在樹進行平衡的時候,也會在加一道鎖

 

               lockRoot();
                        try {
                            root = balanceInsertion(root, x);
                        } finally {
                            unlockRoot();
                        }

 

這樣就保證了在進行平衡操作的時候支持多線程

至於上鎖技術,我們後面多線程單獨開欄目詳細探討

 

 

 4.元素刪除

 元素刪除,參考添加,還有之前treemap,這個也不多bb了好吧,其實是被這個玩意整的有點疲勞。。。

哎,最近寫這個博客耗費太多時間了,也就不再重複的事情上多做功夫,無非還是樹的再平衡,和之前treemap差不多的


5.元素修改和查找

 查找略,參考之前map操作,因爲這個不涉及多線程,所以和之前沒差別

 

6.特殊操作 

 

//獲取node的類對象
            Class<?> ak = Node[].class;
            //可以獲取對象第一個元素的偏移地址
            ABASE = U.arrayBaseOffset(ak);
            //可以獲取數組的轉換因子,也就是數組中元素的增量地址。
            //將arrayBaseOffset與arrayIndexScale配合使用,可以定位數組中每個元素在內存中的位置。
            int scale = U.arrayIndexScale(ak);
            if ((scale & (scale - 1)) != 0) {

                throw new Error("data type scale not a power of two");
            }
            // 給定一個int類型數據,返回這個數據的二進制串中從最左邊算起連續的“0”的總數量。因爲int類型的數據長度爲32所以高位不足的地方會以“0”填充。
            //這個就是計算地址有多少位的長度,也就是一個NODE對象的偏移
            ASHIFT = 31 - Integer.numberOfLeadingZeros(scale);

 

 

    static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
        //讀取tab這個對象中的數據,第二個參數是偏移量,比如第i個,那麼就偏移i個對象的大小ashift
        return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
    }

    //通過cas操作設置值
    static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
                                        Node<K,V> c, Node<K,V> v) {
        //和C比較,設置爲V
        return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
    }

    //設置指定位置的數據爲V
    static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
        U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
    }

 

這個地方是通過unsafe計算出NODE這個類起始位置的偏移量大小,然後通過unsafe計算每個指定的元素的偏移位置,然後把數據值設置進去,unsafe提供硬件級別的操作

 

 

 

 

7.擴容

再put元素的時候,我們添加完元素之後,我們會判斷鏈表長度是否超出8個,如果是轉換爲紅黑樹,然後判斷數組hash桶的長度是否超過64,如果小於64那麼就擴大爲原來的2倍

1.也就是說,擴容發生在鏈表轉換紅黑樹的時候

2.還有一個情況就是添加完畢元素之後,會有個addCount這個方法也會觸發擴容

在轉換爲紅黑樹的時候,我們會調用treeifyBin 方法,這個時候判斷如果長度小於64的時候,會調用tryPresize 這個方法進行擴容

    /**
     * 在同一個節點的個數超過8個的時候,會調用treeifyBin方法來看看是擴容還是轉化爲一棵樹
     * @param tab
     * @param index
     */
    private final void treeifyBin(Node<K,V>[] tab, int index) {
        Node<K,V> b; int n, sc;
        if (tab != null) {
            //先判斷是否這個tab列表的長度小於64
            if ((n = tab.length) < MIN_TREEIFY_CAPACITY) {
                //tryPresize方法把數組長度擴大到原來的兩倍
                tryPresize(n << 1);
            } else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
                synchronized (b) { //使用synchronized同步器,將該節點出的鏈表轉爲樹
                    if (tabAt(tab, index) == b) {
                        TreeNode<K,V> hd = null, tl = null; //hd:樹的頭(head)
                        //循環遍歷鏈表
                        for (Node<K,V> e = b; e != null; e = e.next) {
                            //依次轉換爲treeNode
                            TreeNode<K,V> p = new TreeNode<K,V>(e.hash, e.key, e.val, null, null);
                            //把Node組成的鏈表,轉化爲TreeNode的鏈表,頭結點任然放在相同的位置
                            //這裏吧p的前置節點設置爲上一個節點,也就是尾插法
                            if ((p.prev = tl) == null) {
                                hd = p;
                            } else {
                                tl.next = p;
                            }
                            tl = p;
                        }
                        //吧這個TreeNode組成的鏈表設置進入指定的位置
                        setTabAt(tab, index, new TreeBin<K,V>(hd));
                    }
                }
            }
        }
    }

 

然後進入tryPreSize判斷是直接開始擴容,還是多線程輔助幫助一起擴容

    /**
     * 擴容表爲指可以容納指定個數的大小(總是2的N次方)
     * 假設原來的數組長度爲16,則在調用tryPresize的時候,size參數的值爲16<<1(32),此時sizeCtl的值爲12
     * 計算出來c的值爲64
     *  第一次擴容之後 數組長:32 sizeCtl:24
     *  第二次擴容之後 數組長:64 sizeCtl:48
     *  第二次擴容之後 數組長:128 sizeCtl:94 --> 這個時候纔會退出擴容
     */
    private final void tryPresize(int size) {
        //大小變爲2的冪次,size是原來大小的2倍(擴容的時候)
        /*
         * MAXIMUM_CAPACITY = 1 << 30
         * 如果給定的大小大於等於數組容量的一半,則直接使用最大容量,
         * 否則使用tableSizeFor算出來
         * 後面table一直要擴容到這個值小於等於sizeCtrl(數組長度的3/4)才退出擴容
         */
        int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY : tableSizeFor(size + (size >>> 1) + 1);
        int sc;
        //判斷當前實際容量是否大於0
        while ((sc = sizeCtl) >= 0) {
            Node<K,V>[] tab = table; int n;
            //sc是這個對象的可用實際容量,判斷tab是否被初始化
            if (tab == null || (n = tab.length) == 0) {
                //基礎hash桶沒有被初始化
                //那麼就初始化爲計算出來的c和原來的sc中大的那個
                n = (sc > c) ? sc : c;
                //吧this對象的sizeCtl比較sc判斷是否是sc,如果可以設置爲-1
                if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                    try {
                        //判斷是否當前線程操作
                        if (table == tab) {
                            @SuppressWarnings("unchecked")
                            Node<K,V>[] nt = (Node<K,V>[])new Node<?,?>[n];
                            table = nt;
                            //計算設置新的sc,設置爲容量大小的0.75
                            sc = n - (n >>> 2);
                        }
                    } finally {
                        sizeCtl = sc;
                    }
                }
            } else if (c <= sc || n >= MAXIMUM_CAPACITY) {
                //如果容量c比原來的sc還要小,或者數組長度比最大容量還大那麼就不用擴容了
                break;
            } else if (tab == table) {
                //獲取高位0的個數
                int rs = resizeStamp(n);
                if (sc < 0) {
                    //如果sc<0 說明還在擴容過程中
                    Node<K,V>[] nt;
                    //判斷能否加入一起擴容
                    if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                            sc == rs + MAX_RESIZERS || (nt = nextTable) == null ||
                            transferIndex <= 0) {
                        break;
                    }

                    /*
                     * transfer的線程數加一,該線程將進行transfer的幫忙
                     * 在transfer的時候,sc表示在transfer工作的線程數
                     */
                    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);
                }
            }
        }
    }

最後我們看看真正的擴容邏輯

transfer

 

 

    private final void transfer(Node<K,V>[] tab, Node<K,V>[] nextTab) {
        int n = tab.length, stride;
        //NCPU個數,判斷每個核心處理的個數是否小於最小處理個數,如果是,那麼就設置成16
        //吧數組長度/8然後再除以cpu核心數,如果小於16,那麼就改爲16,如果如果不是小於16,那麼就用這個長度作爲這個線程的處理個數
        if ((stride = (NCPU > 1) ? (n >>> 3) / NCPU : n) < MIN_TRANSFER_STRIDE) {

            stride = MIN_TRANSFER_STRIDE; // subdivide range
        }
        /*
         * 如果複製的目標nextTab爲null的話,則初始化一個table兩倍長的nextTab
         * 此時nextTable被設置值了(在初始情況下是爲null的)
         * 因爲如果有一個線程開始了表的擴張的時候,其他線程也會進來幫忙擴張,
         * 而只是第一個開始擴張的線程需要初始化下目標數組
         */
        if (nextTab == null) {            // initiating
            try {
                //直接創建新的node數組,長度加大一倍
                @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上
            nextTable = nextTab;
            //設置偏移量
            transferIndex = n;
        }
        int nextn = nextTab.length;
        //創建forwardingnode對象,這個是用來控制併發的,當一個節點爲空或已經被轉移之後,就設置爲fwd節點
        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))) {
                    //設置偏移量爲,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) {  //數組中把null的元素設置爲ForwardingNode節點(hash值爲MOVED[-1])

                advance = casTabAt(tab, i, null, fwd);
            } else if ((fh = f.hash) == MOVED) {
                //已經進入擴容
                advance = true; // already processed
            } else {
                //f是指向第i個位置的節點
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                        //下面關鍵是把鏈表拆分爲2個部分
                        Node<K,V> ln, hn;
                        if (fh >= 0) {  //該節點的hash值大於等於0,說明是一個Node節點
                            /*
                             * 因爲n的值爲數組的長度,且是power(2,x)的,所以,在&操作的結果只可能是0或者n
                             * 根據這個規則,我們計算hash位置的的時候
                             * 把高16和低16位進行異或操作
                             * (h ^ (h >>> 16)) & HASH_BITS
                             *  0-->  放在新表的相同位置
                             *  n-->  放在新表的(n+原來位置)
                             *  fh就是指定節點的hash值
                             *  如果還是0那麼就是這個hash值和n的取餘操作還是0,如果是n
                             *  n的值是老數組的長度,用來判斷位置是否改變
                             *  我們取餘的方式使(n-1)&hashcode 因爲n是2的倍數所以是01111111111&hashcode
                             *  因爲是擴大了2倍那麼新的數組的取餘方式其實就是n&hashcode
                             *
                             *  因爲之前取餘是(n-1)&hashcode 然後如果數組擴大2倍,也就是新的鏈表上要定位的話,其實應該是(2n-1)hashcode
                                比之前多了一位,如果還是定位到多的這一位的下面,那麼就不需要進行移動,也就是說有可能造成定位位置不一樣的話,只有2n所在的最高的那一位的1
                                n:000100000000 -》 n-1 : 000011111111
                                2n: 001000000000 -》2n-1: 000111111111
                                也就是我們只要計算hashcode&n就可以知道應該換位置,還是和原來的位置保持一致了
                             */
                            int runBit = fh & n;
                            //指定節點位置
                            Node<K,V> lastRun = f;
                            /*
                             * lastRun 表示的是需要複製的最後一個節點
                             * 每當新節點的hash&n -> b 發生變化的時候,就把runBit設置爲這個結果b
                             * 這樣for循環之後,runBit的值就是最後不變的hash&n的值
                             * 而lastRun的值就是最後一次導致hash&n 發生變化的節點(假設爲p節點)
                             * 爲什麼要這麼做呢?因爲p節點後面的節點的hash&n 值跟p節點是一樣的,
                             * 所以在複製到新的table的時候,它肯定還是跟p節點在同一個位置
                             * 在複製完p節點之後,p節點的next節點還是指向它原來的節點,就不需要進行復制了,自己就被帶過去了
                             * 這也就導致了一個問題就是複製後的鏈表的順序並不一定是原來的倒序
                             * runBit的值就是最後不變的hash&n的值 是值在這個鏈表中的最後一次變化了的位置
                             */
                            for (Node<K,V> p = f.next; p != null; p = p.next) {
                                //p的hash值&n的結果,因爲N是擴大2倍,那麼值錢的node節點如果hash值是小於n的結果對新的位置進行取餘,位置還是原來的位置
                                int b = p.hash & n; //n的值爲擴張前的數組的長度
                                //判斷從這位置開始是否會改變位置,如果老的hash值跟n進行&操作還是保持不變,那麼擴容之後還是原來的位置
                                if (b != runBit) {
                                    runBit = b;
                                    //獲取最後一次變化hash&n的節點位置
                                    lastRun = p;
                                }
                            }
                            //如果runBit==0,說明低位重用
                            if (runBit == 0) {
                                ln = lastRun;
                                hn = null;
                            } else {
                                // 如果最後更新的 runBit 是 1, 設置高位節點
                                hn = lastRun;
                                ln = null;
                            }
                            /*
                             * 構造兩個鏈表,順序大部分和原來是反的
                             * 分別放到原來的位置和新增加的長度的相同位置(i/n+i)
                             */
                            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) {
                                    //如果是0那麼創建節點拼接到ln上
                                    ln = new Node<K,V>(ph, pk, pv, ln);
                                } else {
                                    //否則拼接到hn上
                                    hn = new Node<K,V>(ph, pk, pv, hn);
                                }
                            }

                            //吧鏈表定位到第i個位置上
                            setTabAt(nextTab, i, ln);
                            //吧第二個鏈接到新擴大的節點上n
                            setTabAt(nextTab, i + n, hn);
                            //吧原來的位置的i位置設置爲fwd標識正在擴容
                            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;
                                }
                            }
                            /*
                             * 在複製完樹節點之後,判斷該節點處構成的樹還有幾個節點,
                             * 如果≤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;
                        }
                    }
                }
            }
        }
    }

 

 

還有一點,因爲是多線程安全的,這裏有個特殊操作,當進行擴容的時候,put操作進去會有個helpTransfer的函數,協助擴容,沒錯也就是多線程擴容,這就很NB了

至於擴容的操作我們後面再講,反正這裏需要知道的是這個方法如何參與進去,形成多線程擴容的作用

 

在擴容的時候,有個疑問!!! 

爲什麼int runBit=fh&n;的值可以判斷這個節點位置的鏈表是否需要進行重定位

這個地方我網上找了很多地方都沒有明確說明是爲什麼。

其實要解答這個問題還得從hashMap的取餘方式說起,這個在我之前的hashmap篇有說明

因爲之前取餘是(n-1)&hashcode 然後如果數組擴大2倍,也就是新的鏈表上要定位的話,其實應該是(2n-1)hashcode

比之前多了一位,如果還是定位到多的這一位的下面,那麼就不需要進行移動,也就是說有可能造成定位位置不一樣的話,只有2n所在的最高的那一位的1

n:000100000000 -》 n-1 : 000011111111

2n: 001000000000 -》2n-1: 000111111111

也就是我們只要計算hashcode&n就可以知道應該換位置,還是和原來的位置保持一致了

 

 

 

8.總結

 

 

 

 

參考:

https://www.cnblogs.com/zerotomax/p/8687425.html

 https://segmentfault.com/a/1190000019014835

https://blog.csdn.net/xia744510124/article/details/89478031 

https://blog.csdn.net/zmx729618/article/details/78528227

https://www.cnblogs.com/softidea/p/10261414.html

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章