1. 程式人生 > >分析ConcurrentHashMap的原始碼實現(jdk1.8)

分析ConcurrentHashMap的原始碼實現(jdk1.8)

ConcurrentHashMap不僅實現了多執行緒的同步讀寫而且輕量級,這是它相比於HashMapHashTable的優勢。HashMap是執行緒不安全的,它沒有提供任何的同步機制,多執行緒併發訪問會有問題。HashTable雖然提供了同步機制,但是它是通過整個物件加鎖達到同步的,是重量級的,併發性較低。下面將通過put()和get()方法,分析ConcurrentHashMap的主要實現。

一、ConcurrentHashMap的設計
ConcurrentHashMap將所有的key-value抽象封裝在Node類中,資料儲存結構是這樣的:整體上分桶(table[i]),將不同雜湊值的key-value存放在陣列table[]

的不同位置,即:放在不同的桶中;雜湊值一樣的key-value將以連結串列或者紅黑樹的結構存放在同一個桶中。資料併發一致性通過對桶加鎖和CAS機制實現。本文不分析紅黑樹的初始化、儲存與刪除相關程式碼,ConcurrentHashMap紅黑樹相關程式碼也是其作者改編自CLR,相關演算法可參考紅黑樹詳解
在這裡插入圖片描述

二、ConcurrentHashMap的建構函式
這裡分析ConcurrentHashMap(int initialCapacity);建構函式,其它建構函式大同小異。
1、ConcurrentHashMap中的成員變數介紹:有許多核心的概念要在分析原始碼之前介紹一下,寫在原始碼註釋中:

    //雜湊表的最大長度
private static final int MAXIMUM_CAPACITY = 1 << 30; //雜湊表預設長度 private static final int DEFAULT_CAPACITY = 16; //負載因子,在雜湊表的容量大於陣列長度的3/4時會觸發擴容 private static final float LOAD_FACTOR = 0.75f; //當雜湊表桶中連結串列過長的時候,會觸發樹化,即在連結串列長度大於8的時候,連結串列會轉化為紅黑樹 //理想狀態下,每個桶中元素的數量是符合泊松分佈的,在負載因子為0.75時,一個桶中元素個數大於8個的概率在千萬分之一,
//可以忽略不計,所以設計合理的雜湊表樹化的概率是很低的 static final int TREEIFY_THRESHOLD = 8; //在擴容時,發現樹的長度小於6,紅黑樹會退化為連結串列 static final int UNTREEIFY_THRESHOLD = 6; //連結串列樹化的條件還要陣列長度大於64,雖然桶中元素大於8個,但是陣列長度小於64是不會樹化的,防止雜湊表設計不合理導致樹化 static final int MIN_TREEIFY_CAPACITY = 64; //多個執行緒是可以同時輔助擴容的,一個執行緒最低擴容的區間長度為16 private static final int MIN_TRANSFER_STRIDE = 16; //用來輔助計算sizeCtl值的,在下面的resizeStamp()函式中會介紹 private static final int RESIZE_STAMP_BITS = 16; //在擴容時輔助計算sizeCtl的值,用來表示當前正在擴容的執行緒數 private static final int RESIZE_STAMP_SHIFT = 32 - RESIZE_STAMP_BITS; //ForwardingNode的hash值,代表該節點已經擴容完成了,節點已經被轉移到了新的陣列中 static final int MOVED = -1; //紅黑樹的節點(TreeBin)的hash值 static final int TREEBIN = -2; //掩碼,輔助計算雜湊值,確保雜湊值為正數 static final int HASH_BITS = 0x7fffffff; //機器CPU核心數,在擴容時輔助判斷最多同時工作的執行緒數 static final int NCPU = Runtime.getRuntime().availableProcessors(); //雜湊表中所有的節點存在該陣列中,把每一個位置稱為一個桶 transient volatile Node<K,V>[] table; //擴容時新的陣列,用於將table中的資料copy到該表中 private transient volatile Node<K,V>[] nextTable; //大於0的時候,如果還未初始化table,則代表了陣列長度,否則代表雜湊表的容量(陣列長度的3/4) //-1是陣列正在被初始化 //小於-1的時候,用來表示當前有多少個執行緒在擴容,此時該值是resizeStamp()函式計算出來的, //後16位的實際數值減1代表正在擴容的執行緒(sizeCtl & 0xffff - 1),後面會具體介紹 private transient volatile int sizeCtl;

2、ConcurrentHashMap(int initialCapacity)建構函式:傳入了雜湊表初始陣列長度值,然後根據該值計算sizeCtl 的值。

public ConcurrentHashMap(int initialCapacity) {
        if (initialCapacity < 0)
            throw new IllegalArgumentException();
        //不大於最大容量時,根據initialCapacity計算陣列的初始長度。
        int cap = ((initialCapacity >= (MAXIMUM_CAPACITY >>> 1)) ?
                   MAXIMUM_CAPACITY :
                   tableSizeFor(initialCapacity + (initialCapacity >>> 1) + 1));
        this.sizeCtl = cap;
    }

3、tableSizeFor()函式,用來計算大於c的最小的2^n方的一個數值,因為陣列長度只能是2的冪次方,所以,該方法可以用來找到符合要求的陣列長度值。

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

三、ConcurrentHashMap的put()方法
1、put()方法呼叫了putVal()
該方法主體流程如下:
①如果陣列還沒有初始化,則先初始化陣列,長度為sizeCtl
②計算雜湊值,找到在陣列中的位置,如果該位置為null,代表還沒有資料放入,則直接將key-value封裝成Node節點,放到該位置;
③如果該位置hash==MOVE,代表陣列正在擴容,且此位置已經被移動到新陣列中,則讓該執行緒去輔助擴容;
④該位置是一個連結串列,找到連結串列相應的位置插入節點或者更新節點,在插入後,如果長度大於8,要將連結串列轉換為紅黑樹;
⑤該位置是一棵紅黑樹,則將節點插入樹中,或者更新樹中的節點。

final V putVal(K key, V value, boolean onlyIfAbsent) {
        if (key == null || value == null) throw new NullPointerException();
        //根據key的hashCode值,計算hash值
        int hash = spread(key.hashCode());
        int binCount = 0;
        //這裡是一個for迴圈,停止條件在迴圈裡面,防止一些由於CAS失敗(初始化陣列等)導致插入資料失敗
        for (Node<K,V>[] tab = table;;) {
            Node<K,V> f; int n, i, fh;
            //初始化陣列
            if (tab == null || (n = tab.length) == 0)
                tab = initTable();
            //(n - 1) & hash)計算應該雜湊值,拿到該位置的資料
            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
            }
            //陣列正在擴容,則讓該執行緒去輔助擴容
            else if ((fh = f.hash) == MOVED)
                tab = helpTransfer(tab, f);
            else {
                V oldVal = null;
                //可以看到鎖的粒度比較細,沒有通過對整個表的加鎖完成同步,而是對每一個桶進行加鎖,這樣可以達到桶級別的並行化
                synchronized (f) {
                    if (tabAt(tab, i) == f) {
                    	//hash值大於0,說明該位置是一個連結串列
                        if (fh >= 0) {
                            //記錄連結串列長度
                            binCount = 1;
                            for (Node<K,V> e = f;; ++binCount) {
                                K ek;
                                //遍歷連結串列,如果key已經存在連結串列中,則更新該位置的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;
                                //將key-value插入到連結串列尾部
                                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;
                            }
                        }
                        else if (f instanceof ReservationNode)
                            throw new IllegalStateException("Recursive update");
                    }
                }
                if (binCount != 0) {
                    //連結串列長度大於8,樹化連結串列
                    if (binCount >= TREEIFY_THRESHOLD)
                        treeifyBin(tab, i);
                    if (oldVal != null)
                        return oldVal;
                    break;
                }
            }
        }
        //雜湊表中實際儲存資料長度加1
        addCount(1L, binCount);
        return null;
    }

2、spread()方法計算hash值:將hashCode右移16位,然後再異或,這樣對hash值做了一次擾動,因為計算的雜湊值通常由低位起決定作用(hash&table.legth),經過擾動之後,高位也可以對hash值起到一定的影響,& HASH_BITS之後,保證值為正數。

static final int spread(int h) {
        return (h ^ (h >>> 16)) & HASH_BITS;
}

3、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(); //長度小於0,出錯,放棄執行緒執行
            //CAS,將sizeCtl設定為-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;
                        //將容量設定為陣列長度的3/4
                        sc = n - (n >>> 2);
                    }
                } finally {
                    //將容量設定為陣列長度的3/4
                    sizeCtl = sc;
                }
                break;
            }
        }
        return tab;
    }

4、helpTransfer()函式,用來輔助擴容的函式

final Node<K,V>[] helpTransfer(Node<K,V>[] tab, Node<K,V> f) {
        Node<K,V>[] nextTab; int sc;
        //擴容時,舊陣列table中放入的節點是ForwardingNode型別,nextTable是擴容時新建的陣列,將舊陣列中的資料拷貝到該陣列中
        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) {
                //陣列沒有還未被擴容的區間或者sizeCtl值有變化,則執行緒不需要去輔助擴容
                if ((sc >>> RESIZE_STAMP_SHIFT) != rs || sc == rs + 1 ||
                    sc == rs + MAX_RESIZERS || transferIndex <= 0)
                    break;
                //將sizeCtl加1,代表即將又有一個執行緒去輔助擴容
                if (U.compareAndSwapInt(this, SIZECTL, sc, sc + 1)) {
                    //實際執行擴容的函式
                    transfer(tab, nextTab);
                    break;
                }
            }
            //完成了擴容,返回新陣列
            return nextTab;
        }
        //沒有去輔助擴容,返回舊的陣列
        return table;
    }

5、分析resizeStamp()函式:引數n一般就是陣列長度,RESIZE_STAMP_BITS 是16,Integer.numberOfLeadingZeros(n)函式用於計算一個int型值二進位制中第一個1前面0的個數,然後將該值的第16位置1,在左移16位之後(賦值給sizeCtl),保證是一個負數。如n=16,前置0的個數是27(11011)個,則返回值為00000000000000001000000000011011

static final int resizeStamp(int n) {
        return Integer.numberOfLeadingZeros(n) | (1 << (RESIZE_STAMP_BITS - 1));
    }
    
public static int numberOfLeadingZeros(int i) {
        // HD, Figure 5-6
        if (i == 0)
            return 32;
        int n = 1;
        if (i >>> 16 == 0) { n += 16; i <<= 16; }
        if (i >>> 24 == 0) { n +=  8; i <<=  8; }
        if (i >>> 28 == 0) { n +=  4; i <<=  4; }
        if (i >>> 30 == 0) { n +=  2; i <<=  2; }
        n -= i >>> 31;
        return n;
    }

6、treeifyBin()函式,將連結串列結構轉化為紅黑樹:

private final void treeifyBin(Node<K,V>[] tab, int index) {
        Node<K,V> b; int n;
        if (tab != null) {
            //雖然桶中長度大於8,但是陣列長度小於64,則去擴容,而不是轉化成紅黑樹
            if ((n = tab.length) < MIN_TREEIFY_CAPACITY)
                tryPresize(n << 1);
            else if ((b = tabAt(tab, index)) != null && b.hash >= 0) {
                synchronized (b) {
                    if (tabAt(tab, index) == b) {
                        TreeNode<K,V> hd = null, tl = null;
                        for (Node<K,V> e = b; e != null; e = e.next) {
                            //將連結串列的Node節點轉化成TreeNode節點
                            TreeNode<K,V> p =
                                new TreeNode<K,V>(e.hash, e.key, e.val,
                                                  null, null);
                            if ((p.prev = tl) == null)
                                hd = p;
                            else
                                tl.next = p;
                            tl = p;
                        }
                        //TreeBin的建構函式中會生成紅黑樹的結構
                        setTabAt(tab, index, new TreeBin<K,V>(hd));
                    }
                }
            }
        }
    }

7、tryPresize()擴容從這裡開始,進行擴容前的準備,該函式在正在擴容的時候只會執行一次while 裡的迴圈,它的作用是開啟一次擴容。

private final void tryPresize(int size) {
		//計算擴容後陣列長度,為現在陣列長度的2倍
        int c = (size >= (MAXIMUM_CAPACITY >>> 1)) ? MAXIMUM_CAPACITY :
            tableSizeFor(size + (size >>> 1) + 1);
        int sc;
        //sizeCtl<0說明已經在擴容了,則放棄返回,可以防止該函式在擴容的時候被執行多次
        while ((sc = sizeCtl) >= 0) {
            Node<K,V>[] tab = table; int n;
            //陣列還未初始化,去初始化
            if (tab == null || (n = tab.length) == 0) {
                n = (sc > c) ? sc : c;
                if (U.compareAndSwapInt(this, SIZECTL, sc, -1)) {
                    try {
                        if (table == tab) {
                            @SuppressWarnings("unchecked")
                            Node<K,V>[] nt = (Node<K,V>[])new 
            
           

相關推薦

分析ConcurrentHashMap原始碼實現(jdk1.8)

ConcurrentHashMap不僅實現了多執行緒的同步讀寫而且輕量級,這是它相比於HashMap和HashTable的優勢。HashMap是執行緒不安全的,它沒有提供任何的同步機制,多執行緒併發訪問會有問題。HashTable雖然提供了同步機制,但是它是通過整個物件加鎖達到同步的,是重量

Java執行緒池實現原理與原始碼解析(jdk1.8)

為什麼需要執行緒池? 執行緒池能夠對執行緒進行統一分配,調優和監控: - 降低資源消耗(執行緒無限制地建立,然後使用完畢後銷燬) - 提高響應速度(無須建立執行緒) - 提高執行緒的可管理性 Java是如何實現和管理執行緒池的? 從JDK 5開始,把

PHP實現微信退款的分析原始碼實現

* 1.微信退款到零錢要求必傳證書,需要到https://pay.weixin.qq.com 賬戶中心->賬戶設定->API安全->下載證書,然後修改程式碼中的證書路徑  * 2.該檔案需放到支付授權目錄下,可以在微信支付商戶平臺->產品中心->開發配置中設定。&nb

spring MVC執行過程分析原始碼實現

spring mvc 啟動過程可分為如下3步: =============================================                             web.xml                           Dispa

HashMap原始碼解讀(jdk1.8)

本文基於jdk1.8解讀HashMap關鍵程式碼 HashMap是非執行緒安全的,在多執行緒環境下要使用ConcurrentHashMap 儲存結構 HashMap的儲存結構是陣列 + 連結串列 + 紅黑樹,當連結串列的長度大於等於8時,連結轉成紅黑樹 初始化 HashM

ConcurrentHashMap原始碼實現

concurrentHashmap和HashMap的區別: concurrentHashmap和HashMap大多數下的使用場景基本一致,但最大的區別就是concurrentHashmap是執行緒安全

死磕Java之聊聊HashSet原始碼(基於JDK1.8)

HashSet的UML圖 HashSet的成員變數及其含義 public class HashSet<E> extends AbstractSet<E> implements Set<E>, C

死磕Java之聊聊ArrayList原始碼(基於JDK1.8)

工作快一年了,近期打算研究一下JDK的原始碼,也就因此有了死磕java系列 ArrayList 是一個數組佇列,相當於動態陣列。與Java中的陣列相比,它的容量能動態增長。它繼承於AbstractList,實現了List, RandomAccess, Clo

HashMap原始碼解析jdk1.8:初始化resize,新增put,獲取get

原始碼解析有參考以下部落格: http://www.cnblogs.com/jzb-blog/p/6637823.html HashMap:   以k-v鍵值對儲存格式的容器,key,value都可以為空,key不重複,非執行緒安全(執行緒安全請使用Concur

HashMap原始碼解析(jdk1.8)

寫在篇頭 其實這是在我寫完下邊所有方法解析後寫的。每次看原始碼,有些時候都不知道每一步的意義在哪裡,缺少了自己的思考,直接看會枯燥,甚至不知所云。今天突然想換種說明方式。為什麼會有HashMap這種結構,為了實現什麼目的?為什麼用這種結構?比其他結構的好在哪裡

CentOS7.5原始碼安裝JDK1.8詳細過程

第一步  解除安裝系統自帶的OpenJDK以及相關的java檔案 用root管理員登入伺服器 ①在命令視窗鍵入: java -version 可以看到系統自帶的OpenJDK版本資訊。 ②在命令視窗鍵入: rpm -qa | grep java 命令說明:

Linux原始碼安裝JDK1.8

Linux原始碼安裝Java 1、到官網下載 jdk-8u131-linux-x64.tar.gz 官網地址:http://www.oracle.com/technetwork/java/javase/downloads/jdk8-downloads-21

原始碼解析JDK1.8-HashMap連結串列成環的問題解決方案

前言   上篇文章詳解介紹了HashMap在JDK1.7版本中連結串列成環的原因,今天介紹下JDK1.8針對HashMap執行緒安全問題的解決方案。 jdk1.8 擴容原始碼解析 public class HashMap<K,V> extends AbstractMap<K,V>&n

JDK1.8 ConcurrentHashMap原始碼分析

文章目錄 ConcurrentHashMap資料結構 類的繼承關係 類的內部類 重要的屬性 類的建構函式 ConcurrentHashMap()型建構函式 ConcurrentHashMap(i

ConcurrentHashMap JDK1.8中結構原理及原始碼分析

注:本文根據網路和部分書籍整理基於JDK1.7書寫,如有雷同敬請諒解  歡迎指正文中的錯誤之處。 資料結構       ConcurrentHashMap 1.8 拋棄了Segment分段鎖機制,採用Node + CAS + Synchronized來保證併發安全進行實現

Java中HashMap底層實現原理(JDK1.8)原始碼分析

在JDK1.6,JDK1.7中,HashMap採用位桶+連結串列實現,即使用連結串列處理衝突,同一hash值的連結串列都儲存在一個連結串列裡。但是當位於一個桶中的元素較多,即hash值相等的元素較多時,通過key值依次查詢的效率較低。而JDK1.8中,HashMap採用位桶+

(轉載)Java中HashMap底層實現原理(JDK1.8)原始碼分析

近期在看一些java底層的東西,看到一篇分析hashMap不錯的文章,跟大家分享一下。 在JDK1.6,JDK1.7中,HashMap採用位桶+連結串列實現,即使用連結串列處理衝突,同一hash值的連結串列都儲存在一個連結串列裡。但是當位於一個桶中的元素較多,即hash值

JDK1.8中ArrayList的實現原理及原始碼分析

一、概述              ArrayList是Java開發中使用比較頻繁的一個類,通過對原始碼的解讀,可以瞭解ArrayList的內部結構以及實現方法,清楚它的優缺點,以便我們在程式設計時靈活運用。 二、原始碼分析 2.1 類結構  JDK1.8原始碼中的A

Java中HashMap底層實現原理(JDK1.8)源碼分析

blank imp dash logs || 屬性 lte das ces 這幾天學習了HashMap的底層實現,但是發現好幾個版本的,代碼不一,而且看了Android包的HashMap和JDK中的HashMap的也不是一樣,原來他們沒有指定JDK版本,很多文章都是舊版本J

ConcurrentHashMapJDK1.8實現

也說 con ping 線程 shc 正在 nali break 頭部 今天我們介紹一下ConcurrentHashMap在JDK1.8中的實現。基本結構 ConcurrentHashMap在1.8中的實現,相比於1.7的版本基本上全部都變掉了。首