JDK併發工具類源碼學習系列——ConcurrentHashMap

歡迎閱讀本系列更多文章:JDK併發工具類源碼學習系列目錄
作爲JDK併發工具類源碼學習系列的第一個被分析的類,ConcurrentHashMap類在我的開發過程中經常被使用。個人覺得如果在共享一個Map時,如果無法判斷是否需要加鎖,那麼就乾脆直接使用ConcurrentHashMap,即能保證併發安全,同時性能也不會有太多下降,因爲ConcurrentHashMap可實現無鎖讀,不過內存會佔用的多些,但是並不明顯,基本可以忽略。
下面我們就來看看ConcurrentHashMap類的內部構造。

1. 結構預覽

1. 類定義
public class ConcurrentHashMap<K, V> extends AbstractMap<K, V> implements ConcurrentMap<K, V>, Serializable

上面是ConcurrentHashMap類的定義,從ConcurrentHashMap的定義可以看出ConcurrentHashMap是實現了ConcurrentMap接口,而非直接實現Map接口。同時ConcurrentMap的子接口還有一個ConcurrentNavigableMap,表示可支持導航的併發Map。可見ConcurrentMap接口定義可支持併發,NavigableMap接口定義可支持導航,SortedMap接口定義可支持排序,NavigableMap繼承自SortedMap。從Map的API介紹可以看出Java Collections Framework家族中重要一員——Map的組織結構——通過接口定義Map的行爲,或者說Map可支持的功能,多個接口之間可交叉,如ConcurrentNavigableMap即實現ConcurrentMap接口又實現NavigableMap接口。

2. 類結構

ConcurrentHashMap結構圖
從圖中可以看出ConcurrentHashMap內部包含了多個內部類,其中最重要的也是我們最需要關心的是:SegmentHashEntry
Segment是ConcurrentHashMap非常重要的一個內部類,是ConcurrentHashMap實現高併發的關鍵點,Segment在ConcurrentHashMap中承擔着所有的操作,即所有對ConcurrentHashMap的操作最終都會對Segment進行操作。因爲Segment保存了最終的數據,而ConcurrentHashMap只是保存了一個Segment的數組。ConcurrentHashMap通過N個Segment將數據切分成N塊,而每塊之間是互不影響的,所以理論上可以同時並行的執行N個需要加鎖的操作,這就是ConcurrentHashMap併發的基礎。
HashEntry同HashMap中的Entry,每個HashEntry是一個節點,保存key和value,以及下一個節點。HashEntry中的key,hash 和 next 域都被聲明爲 final 型,value 域被聲明爲 volatile 型,可見HashEntry類的value是可變的,其他的key和next都是不可變的。
EntryIterator,EntrySet,HashIterator,KeyIterator,KeySet,ValueIterator,Values是輔助ConcurrentHashMap實現遍歷的內部類。
下面簡單介紹下SegmentHashEntry類。
HashEntry

static final class HashEntry<K,V> {
        final K key;
        final int hash;
        volatile V value;
        final HashEntry<K,V> next;

        HashEntry(K key, int hash, HashEntry<K,V> next, V value) {
            this.key = key;
            this.hash = hash;
            this.next = next;
            this.value = value;
        }

        @SuppressWarnings("unchecked")
        static final <K,V> HashEntry<K,V>[] newArray(int i) {
            return new HashEntry[i];
        }
    }

HashEntry類的結構很簡單,就是四個變量,一個構造函數,一個static方法。由於沒有任何getter和setter方法,所以對其操作是直接訪問變量。在 ConcurrentHashMap 中,在散列時如果產生“碰撞”,將採用“分離鏈接法”來處理“碰撞”:把“碰撞”的 HashEntry 對象鏈接成一個鏈表。由於 HashEntry 的 next 域爲 final 型,所以新節點只能在鏈表的表頭處插入。所以鏈表中節點的順序和插入的順序相反。
Segment

static final class Segment<K,V> extends ReentrantLock implements Serializable

Segment繼承自ReentrantLock ,所以它可以作爲一個鎖使用,其在ConcurrentHashMap也正是作爲一個鎖來使用的。

transient volatile int count;//Segment中保存的元素數量
transient int modCount;//記錄Segment被修改的次數,用於在讀取時判斷讀取期間改Segment是否有過修改,有的話則重試
transient int threshold;//閥值,元素數量達到該值則會進行自動擴展
transient volatile HashEntry<K,V>[] table;//桶,一個HashEntry的數組,按HashCode值散列保存,採用鏈表解決hash碰撞問題
final float loadFactor;//負載因子

count 變量是一個計數器,它表示每個 Segment 對象管理的 table 數組(若干個 HashEntry 組成的鏈表)包含的 HashEntry 對象的個數。每一個 Segment 對象都有一個 count 對象來表示本 Segment 中包含的 HashEntry 對象的總數。注意,之所以在每個 Segment 對象中包含一個計數器,而不是在 ConcurrentHashMap 中使用全局的計數器,是爲了避免出現“熱點域”而影響 ConcurrentHashMap 的併發性。
Segment結構
從Segment擁有的方法可以看出,針對ConcurrentHashMap的操作基本上都會調用具體某個Segment的對應方法,如put會調用Segment的put方法。所以Segment是最終的操作類。

下圖是依次插入 ABC 三個 HashEntry 節點後,Segment 的結構示意圖。
插入三個節點後 Segment 的結構示意圖
Segment的方法會在介紹ConcurrentHashMap的方法時進行解釋,這裏先不介紹。

2. 構造器解讀

public ConcurrentHashMap(int initialCapacity, float loadFactor) {
        this(initialCapacity, loadFactor, DEFAULT_CONCURRENCY_LEVEL);
    }
public ConcurrentHashMap(int initialCapacity) {
        this(initialCapacity, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
    }
public ConcurrentHashMap() {
        this(DEFAULT_INITIAL_CAPACITY, DEFAULT_LOAD_FACTOR, DEFAULT_CONCURRENCY_LEVEL);
    }

以上的構造器都只是一個個重載函數,最終都會調用下面的構造器。其中使用到了三個常量:

  • DEFAULT_INITIAL_CAPACITY:默認初始容量
  • DEFAULT_LOAD_FACTOR:默認加載因子
  • DEFAULT_CONCURRENCY_LEVEL:默認併發級別,該值決定一個包含多少個Segment,即將ConcurrentHashMap切分成多少塊
public ConcurrentHashMap(int initialCapacity,
                             float loadFactor, int concurrencyLevel) {
        if (!(loadFactor > 0) || initialCapacity < 0 || concurrencyLevel <= 0)
            throw new IllegalArgumentException();

        if (concurrencyLevel > MAX_SEGMENTS)
            concurrencyLevel = MAX_SEGMENTS;

        // Find power-of-two sizes best matching arguments
        int sshift = 0;
        int ssize = 1;
        while (ssize < concurrencyLevel) {
            ++sshift;
            ssize <<= 1;
        }
        segmentShift = 32 - sshift;
        segmentMask = ssize - 1;
        this.segments = Segment.newArray(ssize);

        if (initialCapacity > MAXIMUM_CAPACITY)
            initialCapacity = MAXIMUM_CAPACITY;
        int c = initialCapacity / ssize;
        if (c * ssize < initialCapacity)
            ++c;
        int cap = 1;
        while (cap < c)
            cap <<= 1;

        for (int i = 0; i < this.segments.length; ++i)
            this.segments[i] = new Segment<K,V>(cap, loadFactor);
    }

該構造函數需要制定初始容量、加載因子以及併發級別,對應上面提到的三個常量(默認值)。代碼前幾句是對參數進行正確性校驗。// Find power-of-two sizes best matching arguments這句註釋的意思是尋找一個參數的最佳匹配值:最接近指定的參數的2的冪方值。下面我們對照着代碼來說明這句話的含義:

// Find power-of-two sizes best matching arguments
int sshift = 0;
int ssize = 1;
while (ssize < concurrencyLevel) {
    ++sshift;
    ssize <<= 1;
}

這裏定義了一個ssize變量,該變量就是concurrencyLevel的最佳匹配值,可以看見首先是循環,直到ssize>=concurrencyLevel,所以最佳匹配值是大於等於指定參數的,循環裏面每次會將ssize右移一位,即*2,所以最終得到的值就是一個最接近且大於等於concurrencyLevel的2次冪方值。同時定義了一個sshift變量,該變量隨着ssize的每次右移而+1,最終得到的即是ssize是2的多少次方,即sszie=2^sshift。繼續往下看:

segmentShift = 32 - sshift;//偏移量
segmentMask = ssize - 1;//掩碼值
this.segments = Segment.newArray(ssize);//初始化segments數組

segmentShift以及segmentMask在後面將一個hash映射到某一個segments時使用,目的是將hash均勻的分配到每個segments,具體爲什麼使用這兩個來進行均勻分配我們這裏不介紹。最後一句是初始化一個segments數組,大小是ssize,而非參數concurrencyLevel值。下面繼續看:

if (initialCapacity > MAXIMUM_CAPACITY)
    initialCapacity = MAXIMUM_CAPACITY;
int c = initialCapacity / ssize;
if (c * ssize < initialCapacity)
    ++c;
int cap = 1;
while (cap < c)
    cap <<= 1;

for (int i = 0; i < this.segments.length; ++i)
    this.segments[i] = new Segment<K,V>(cap, loadFactor);

initialCapacity是構造器指定的初始化容量,ssize是segments數組大小,所以c的值就是每個segments的容量。下面定義了一個cap,這裏的cap和前面的ssize是一個含義,即選擇一個最接近且大於等於c的2的冪方值,然後初始化segments數組,傳入的參數有cap(segment容量)和loadFactor(負載因子)。這裏選擇cap作爲segment容量,而非c,是出於方便後期對segment的容量進行擴充考慮,如果容量是2的冪方,那麼想要將容量擴充一倍只需右移1位即可,同時保證依舊是2的冪方。
對於segment的初始化很簡單,對loadFactor賦值,然後根據指定的初始容量創建一個HashEntry數組,並計算出threshold(閥值,當segment中的元素超過這個閾值則進行容量擴充)。

3. 常用方法解讀

ConcurrentHashMap實現了Map接口,那麼他的核心方法包括我們常用的put(K, V)、get(Object)、remove(Object)、contains(Object)、size(),同時繼承自ConcurrentMap讓他包含了putIfAbsent(K, V)、remove(Object, Object)、replace(K, V, V)、replace(K, V)四個併發方法。後面的四個併發方法是ConcurrentMap爲我們提供的在併發情景下使用的工具方法,都是基於CAS來實現的。
在看put(K, V)、get(Object)等方法實現之前,先來看下這兩個方法:hash(int)和segmentFor(int)。

 /* ---------------- Small Utilities -------------- */

/**
 * Applies a supplemental hash function to a given hashCode, which
 * defends against poor quality hash functions.  This is critical
 * because ConcurrentHashMap uses power-of-two length hash tables,
 * that otherwise encounter collisions for hashCodes that do not
 * differ in lower or upper bits.
 */
private static int hash(int h) {
    // Spread bits to regularize both segment and index locations,
    // using variant of single-word Wang/Jenkins hash.
    h += (h <<  15) ^ 0xffffcd7d;
    h ^= (h >>> 10);
    h += (h <<   3);
    h ^= (h >>>  6);
    h += (h <<   2) + (h << 14);
    return h ^ (h >>> 16);
}

/**
 * Returns the segment that should be used for key with given hash
 * @param hash the hash code for the key
 * @return the segment
 */
final Segment<K,V> segmentFor(int hash) {
    return segments[(hash >>> segmentShift) & segmentMask];
}

源碼中對這兩個方法的註釋是:Small Utilities,即小工具方法。源碼中對於hash方法的註釋的意思是:該方法是一個補充hash方法,ConcurrentHashMap的hash表的長度是2的冪方,使用該補充hash函數可降低一些質量差的hash函數發生的碰撞概率。具體如何實現的就不看了,就算看懂了代碼也很難理解這樣做的原因,所以不浪費時間。segmentFor是爲一個hash值找到它應該去的segment,這裏使用到了segmentShift以及segmentMask,還記得segmentShift是32-sshift,這裏將hash值無符號左移segmentShift位,即取hash值的高sshift位,然後同segmentMask按位與運算。其實就是取hash值的高sshift位將值限制在0~ssize之間,然後與ssize-1取餘得到segments數組的下標(取高位是因爲更加均勻,低位的重複率比高位高,臆測~!!!)。
瞭解了上面兩個方法,下面我們就來看看put(K, V)、get(Object)、remove(Object)這三個方法的具體實現。

1. put(K, V)
public V put(K key, V value) {
    if (value == null)
        throw new NullPointerException();
    int hash = hash(key.hashCode());
    return segmentFor(hash).put(key, hash, value, false);
}

ConcurrentHashMap的put方法內部只是根據key的hash值找到對應的Segement,然後調用Segement的put方法,注意Segement的put方法的第四個參數,這裏穿的值是false。我們主要分析下Segement的put方法。Segement在這裏的作用就是將元素均勻分成N等份,各個Segement之間互不干擾,讀寫也不會發生衝突,降低併發要求。

V put(K key, int hash, V value, boolean onlyIfAbsent) {
    lock();
    try {
        int c = count;
        if (c++ > threshold) // ensure capacity
            rehash();
        HashEntry<K,V>[] tab = table;
        int index = hash & (tab.length - 1);
        HashEntry<K,V> first = tab[index];
        HashEntry<K,V> e = first;
        while (e != null && (e.hash != hash || !key.equals(e.key)))
            e = e.next;

        V oldValue;
        if (e != null) {
            oldValue = e.value;
            if (!onlyIfAbsent)
                e.value = value;
        }
        else {
            oldValue = null;
            ++modCount;
            tab[index] = new HashEntry<K,V>(key, hash, first, value);
            count = c; // write-volatile
        }
        return oldValue;
    } finally {
        unlock();
    }
}   

首先第一步就是lock,看來再NB的併發類在寫時也需要lock啊。讀取count值,從count的註釋可以看出該值是記錄Segment包含的元素數量,volatile修飾的(這裏利用了volatile變量的內存可見性)。然後判斷增加之後元素數量是否超過閾值,超過的話提前擴容。接着找到該hash對應的table(桶),簡單的取餘操作。找到該table的第一個元素——first,因爲ConcurrentHashMap使用鏈表來解決hash衝突問題,所以這裏的table是一個鏈表。

while (e != null && (e.hash != hash || !key.equals(e.key)))
      e = e.next;

通過循環,並通過比較hash值以及equals()校驗,尋找與key相同的已存在的元素。

 V oldValue;
 if (e != null) {
     oldValue = e.value;
     if (!onlyIfAbsent)
         e.value = value;
 }
 else {
     oldValue = null;
     ++modCount;
     tab[index] = new HashEntry<K,V>(key, hash, first, value);
     count = c; // write-volatile
 }

e!=null說明找到與要插入的元素key相同的元素,那麼onlyIfAbsent=false則直接將原元素的value值替換,返回原值,由於HashEntry的value是volatile的,所以修改之後會立即被後續線程可見;onlyIfAbsent=true則不做任何操作。e==null時,modCount自增(modCount記錄了對該Segment的進行的結構性修改的次數,modCount值使得在進行批量讀取時能夠知道在讀取期間Segment結構是否被修改來決定是否進行加鎖讀取)。tab[index] = new HashEntry(key, hash, first, value)這句就是將被插入的元素添加到鏈表中,但是插入的位置是頭部,而非尾部。HashEntry的構造器傳入一個HashEntry對象,該對象是鏈表原來的頭部,被作爲新創建的節點的next指針,所以新的鏈表的頭部元素是新增加的,後面接着是原來的鏈表。
注意:此處的lock並非對整個Map進行加鎖,而只是對該Segment進行加鎖,所以如果一個線程進行put操作,其他的另外15個(ssize-1)Segment仍是可訪問的。

2. remove(Object)
public V remove(Object key) {
    int hash = hash(key.hashCode());
    return segmentFor(hash).remove(key, hash, null);
}

/**
 * Remove; match on key only if value null, else match both.
 */
V remove(Object key, int hash, Object value) {
    //由於remove是結構性修改,所以第一步便是lock
    lock();
    try {
        //讀取count值,此處是利用volatile變量的內存可見性來保證讀線程能夠及時的讀取到最新值(後面會單獨介紹)
        int c = count - 1;
        //是根據key的hashCode找到該節點對應的桶
        HashEntry<K,V>[] tab = table;
        int index = hash & (tab.length - 1);
        HashEntry<K,V> first = tab[index];
        HashEntry<K,V> e = first;
        //循環找到該節點
        while (e != null && (e.hash != hash || !key.equals(e.key)))
            e = e.next;

        V oldValue = null;
        if (e != null) {
        //找到待刪除節點
            V v = e.value;
            //如果value==null,則無需關心節點的值是否與指定值相同,否則只有在兩者相同情況纔可刪除
            if (value == null || value.equals(v)) {
                oldValue = v;
                // All entries following removed node can stay
                // in list, but all preceding ones need to be
                // cloned.
                ++modCount;
                HashEntry<K,V> newFirst = e.next;
                for (HashEntry<K,V> p = first; p != e; p = p.next)
                    newFirst = new HashEntry<K,V>(p.key, p.hash,
                                                  newFirst, p.value);
                tab[index] = newFirst;
                count = c; // write-volatile
            }
        }
        return oldValue;
    } finally {
        unlock();
    }
}

依舊調用的是對應的Segment的remove()方法。由於remove是結構性修改,所以需要進行加鎖操作。在刪除一個節點時,爲了不影響正在遍歷鏈表的線程,這裏採用了複製方式,而非直接移除待刪除節點。具體工作方式:將待刪除節點之後的節點不動,而待刪除節點之後的節點複製到另外一個鏈表,看代碼:HashEntry<K,V> newFirst = e.next;這句將待刪除節點的next節點賦值給newFirst for (HashEntry<K,V> p = first; p != e; p = p.next)此處的for循環從鏈表的頭部開始一直循環到待刪除節點爲止,newFirst = new HashEntry<K,V>(p.key, p.hash, newFirst, p.value);for循環內部根據當前循環的節點新建了一個key和value、hash都相同的節點,不同的是next指向了前一個新建的節點(第一個newFirst是待刪除節點的下一個節點),即構成了一個以待刪除節點的前一個節點爲頭結點的新的鏈表,然後tab[index] = newFirst;將該鏈表賦到對應的桶上,便完成了整個刪除操作,最終新的鏈表以待刪除節點的前一個節點爲頭結點。
下面通過圖例來說明 remove 操作。假設寫線程執行 remove 操作,要刪除鏈表的 C 節點,另一個讀線程同時正在遍歷這個鏈表。
執行刪除之前的原鏈表:
執行刪除之前的原鏈表
執行刪除之後的新鏈表:
執行刪除之後的新鏈表
從圖中可以看出被刪除節點之後的節點原封不動保留在鏈表中,而之前的鏈表從後往前依次被複制到新的鏈表中,但是原鏈表在我們進行remove操作過程中始終是會發生任何變化的,所以寫線程對某個鏈表進行remove操作不會影響其他的併發讀線程對這個鏈表的遍歷訪問。

3. get(Object)
public V get(Object key) {
    int hash = hash(key.hashCode());
    return segmentFor(hash).get(key, hash);
}

ConcurrentHashMap的get()方法同put()一樣,也是依賴於Segment的get()方法。下面看看Segment的get()方法

V get(Object key, int hash) {
    if (count != 0) { // read-volatile
        HashEntry<K,V> e = getFirst(hash);
        while (e != null) {
            if (e.hash == hash && key.equals(e.key)) {
                V v = e.value;
                if (v != null)
                    return v;
                return readValueUnderLock(e); // recheck
            }
            e = e.next;
        }
    }
    return null;
}

 /**
 * Returns properly casted first entry of bin for given hash.
  */
 HashEntry<K,V> getFirst(int hash) {
     HashEntry<K,V>[] tab = table;
     return tab[hash & (tab.length - 1)];
 }

/**
 * Reads value field of an entry under lock. Called if value
 * field ever appears to be null. This is possible only if a
 * compiler happens to reorder a HashEntry initialization with
 * its table assignment, which is legal under memory model
 * but is not known to ever occur.
 */
V readValueUnderLock(HashEntry<K,V> e) {
    lock();
    try {
        return e.value;
    } finally {
        unlock();
    }
}

從代碼可以看到get()方法在讀取時無需進行加鎖操作,除非讀取到的值爲NULL。爲什麼讀取一個節點的值爲NULL的時候需要加鎖呢?因爲ConcurrentHashMap是不允許NULL作爲key或者value的,所以是不應該出現讀取一個節點的值爲NULL的情況,如果出現這種情況,說明出現了併發問題,所以加上鎖再次讀取!(什麼情況下會出現這種情況並不清楚)。

4. 總結

ConcurrentHashMap在進行結構性修改,如put/remove/replace時都需要進行加鎖,但是讀取並未加鎖,併發情況下,由於內存不同步問題,會導致一個線程的寫操作並不會立即對另一個線程可見。這裏ConcurrentHashMap通過volatile變量的內存可見性特性來保證一個線程的寫操作立即被其他線程可見,每個方法在一開始都會讀取count這個變量,該變量就是一個volatile變量,多個線程之間通過讀寫這個變量來保證內存可見性,具體可參考下方的關於JVM內存可見性的說明。
上面三個方法基本包含了整個ConcurrentHashMap的讀寫操作(replace(K, V)方法只是簡單的更新節點的value值,由於value是volatile的,所以也不會影響讀線程),從三個方法的分析來看ConcurrentHashMap首先通過Segment對整個數據集進行切分,並通過對各個部分的數據集進行加鎖來提高整個數據集的併發性;通過讀寫分離的方式實現無鎖讀,加鎖寫,進一步提高ConcurrentHashMap的讀寫效率;並通過volatile變量的特性實現讀寫的可見性保證。

4. 使用場景

ConcurrentHashMap由於其即使在同步的情況下依舊保證高效的讀寫性能,所以在很多需要使用HashMap的情況都適用,當然單線程情況並不需要使用同步的ConcurrentHashMap。如果無法保證你的HashMap只是在單線程情況下使用那麼就使用ConcurrentHashMap,因爲其在單線程情況下的效率也並不低。
下面是針對單線程環境下ConcurrentHashMap和HashMap的put性能的對比:
硬件PC:普通PC機,i5
JVM:內存1G
測試數據:執行10次,計算均值
結果:表格

Map PUT1W次 PUT10W次 PUT100W次
ConcurrentHashMap 2175317 28068193 1355076232
HashMap 1201131 28068193 407341713

Java 內存模型

由於 ConcurrentHashMap 是建立在 Java 內存模型基礎上的,爲了更好的理解 ConcurrentHashMap,讓我們首先來了解一下 Java 的內存模型。
Java 語言的內存模型由一些規則組成,這些規則確定線程對內存的訪問如何排序以及何時可以確保它們對線程是可見的。下面我們將分別介紹 Java 內存模型的重排序,內存可見性和 happens-before 關係。

重排序

內存模型描述了程序的可能行爲。具體的編譯器實現可以產生任意它喜歡的代碼 – 只要所有執行這些代碼產生的結果,能夠和內存模型預測的結果保持一致。這爲編譯器實現者提供了很大的自由,包括操作的重排序。
編譯器生成指令的次序,可以不同於源代碼所暗示的“顯然”版本。重排序後的指令,對於優化執行以及成熟的全局寄存器分配算法的使用,都是大有脾益的,它使得程序在計算性能上有了很大的提升。
重排序類型包括:
- 編譯器生成指令的次序,可以不同於源代碼所暗示的“顯然”版本。
- 處理器可以亂序或者並行的執行指令。
- 緩存會改變寫入提交到主內存的變量的次序。

內存可見性

由於現代可共享內存的多處理器架構可能導致一個線程無法馬上(甚至永遠)看到另一個線程操作產生的結果。所以 Java 內存模型規定了 JVM 的一種最小保證:什麼時候寫入一個變量對其他線程可見。
在現代可共享內存的多處理器體系結構中每個處理器都有自己的緩存,並週期性的與主內存協調一致。假設線程 A 寫入一個變量值 V,隨後另一個線程 B 讀取變量 V 的值,在下列情況下,線程 B 讀取的值可能不是線程 A 寫入的最新值:
- 執行線程 A 的處理器把變量 V 緩存到寄存器中。
- 執行線程 A 的處理器把變量 V 緩存到自己的緩存中,但還沒有同步刷新到主內存中去。
- 執行線程 B 的處理器的緩存中有變量 V 的舊值。

Happens-before 關係

happens-before 關係保證:如果線程 A 與線程 B 滿足 happens-before 關係,則線程 A 執行動作的結果對於線程 B 是可見的。如果兩個操作未按 happens-before 排序,JVM 將可以對他們任意重排序。
下面介紹幾個與理解 ConcurrentHashMap 有關的 happens-before 關係法則:
1. 程序次序法則:如果在程序中,所有動作 A 出現在動作 B 之前,則線程中的每動作 A 都 happens-before 於該線程中的每一個動作 B。
2. 監視器鎖法則:對一個監視器的解鎖 happens-before 於每個後續對同一監視器的加鎖。
3. Volatile 變量法則:對 Volatile 域的寫入操作 happens-before 於每個後續對同一 Volatile 的讀操作。
4. 傳遞性:如果 A happens-before 於 B,且 B happens-before C,則 A happens-before C。

以上摘自探索 ConcurrentHashMap 高併發性的實現機制


參考文章

1.探索 ConcurrentHashMap 高併發性的實現機制
2.聊聊併發(四)——深入分析ConcurrentHashMap


歡迎訪問我的個人博客~~~

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