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

更多文章請閱讀:JDK併發工具類源碼學習系列目錄
ConcurrentSkipListMap在JDK併發工具類使用範圍不是很廣,它是針對某一特殊需求而設計的——支持排序,同時支持搜索目標返回最接近匹配項的導航方法。一般情況下開發者很少會使用到該類,但是如果你有如上的特殊需求,那麼ConcurrentSkipListMap將是一個很好地解決方案。
Java Collections Framework中另一個支持排序的Map是TreeMap,兩者都是有序的哈希表,但是TreeMap是非線程安全的,當然可以使用Collections.synchronizedSortedMap將TreeMap進行包裝,但是性能方面就顯得捉急了,畢竟多個線程一起讀都需要加鎖是不太合理的,至少做到讀寫分離呀。但是從JDK1.6開始我們就多了一個選擇:ConcurrentSkipListMap。這是一個支持高併發度的有序哈希表,並且是無鎖的,可在多線程環境下替代TreeMap。
下面我們從幾個方面來分析ConcurrentSkipListMap的實現,主要分析ConcurrentSkipListMap的數據結構以及如何實現高併發且無鎖讀寫的。

  • 實現原理
  • 數據結構
  • 常用方法解讀
  • 使用場景
實現原理

ConcurrentSkipListMap不同於TreeMap,前者使用SkipList(跳錶)實現排序,而後者使用紅黑樹。相比紅黑樹,跳錶的原理比較容易理解,簡單點說就是在有序的鏈表上使用多級索引來定位元素。下面是簡單看看SkipList的原理:

現在假設我們擁有一個有序的列表,如果我們想要在這個列表中查找一個元素,最優的查找算法應該是二分 查找了,但是鏈表不想數組,鏈表的元素是非連續的,所以無法使用二分查找來進行高效查找,那麼拋開其他查找算法不說,最低效的算法就是從鏈表頭部開始遍歷,直至找到要查找的元素,雖說低效但是確是最大化使用了鏈表的優勢——遍歷。那麼我們有沒有可能同時提高查找效率,而且還使用鏈表的優勢呢?跳錶就是我們想要的查找算法。其實說白了跳錶就是一種多級索引,通過索引鏈表中的部分值來確定被查找的值所處的範圍。

跳錶分爲多層,層數越高,最高層元素越少,查找時也會越快,但是所佔空間也就越多,所以跳錶是用空間換時間(可見如果大量數據不是太適用,畢竟內存是有限的)。

跳錶結構示意圖:

跳錶

可以看見每一層都是一個有序的鏈表,而且是原始鏈表的子集,最底層(level1)是完整的鏈表結構,越往上鍊表的元素越少,同時查找也就越快。當我們需要查找一個元素時,會從跳錶的最上層鏈表開始查詢,定位元素的大致位置,然後通過向下指針再在下層查找。

跳錶查找示意圖:

跳錶查找示意圖

上圖是從跳錶中查找32的過程。
跳錶的查找時間複雜度是:O(log(n)),更多詳細關於跳錶的介紹可參考:http://kenby.iteye.com/blog/1187303

數據結構

ConcurrentSkipListMap在原始鏈表的基礎上增加了跳錶的結構,所以需要兩個額外的內部類來封裝鏈表的節點,以及跳錶的節點——NodeIndex

Node:鏈表的節點
static final class Node<K,V> {
    final K key;
    volatile Object value;
    volatile Node<K,V> next;
}

同ConcurrentHashMap的Node節點一樣,key爲final,是不可變的,value和next通過volatile修飾保證內存可見性。

Index:跳錶的節點
static class Index<K,V> {
    final Node<K,V> node;
    final Index<K,V> down;
    volatile Index<K,V> right;
}

Index封裝了跳錶需要的結構,首先node包裝了鏈表的節點,down指向下一層的節點(不是Node,而是Index),right指向同層右邊的節點。node和down都是final的,說明跳錶的節點一旦創建,其中的值以及所處的層就不會發生變化(因爲down不會變化,所以其下層的down都不會變化,那他的層顯然不會變化)。
Node和Index內部都提供了用於CAS原子更新的AtomicReferenceFieldUpdater對象,至於該對象的原理下面是不會深入研究的。

常用方法解讀

從API文檔可以看到ConcurrentHashMap的方法很多,很多都是一些爲了方便開發者的提供的,例如subMap(K, boolean, K, boolean)、headMap(K, boolean)、tailMap(K, boolean)等都是用來返回一個子視圖的方法。這裏我們主要看看能夠表達ConcurrentHashMap實現原理的三個方法:put(K, V)、get(Object)、remove(Object, Object)。
在介紹這三個方法之前,我們先看一個輔助工具方法:comparable(Object)。

/**
 * If using comparator, return a ComparableUsingComparator, else
 * cast key as Comparable, which may cause ClassCastException,
 * which is propagated back to caller.
 */
// @By Vicky:將key封裝成一個Comparable對象
private Comparable<? super K> comparable(Object key) throws ClassCastException {
    if (key == null)
        throw new NullPointerException();
    // @By Vicky:有兩種封裝方法,如果在構造時指定了comparator,則使用comparator封裝key
    // 如果沒有指定comparator,則key必須是一個繼承自Comparable接口的類,否則會拋出ClassCastException
    // 所以ConcurrentSkipListMap的key要麼是繼承自Comparable接口的類,如果不是的話需要顯示提供comparator進行比較
    if (comparator != null)
        return new ComparableUsingComparator<K>((K)key, comparator);
    else
        return (Comparable<? super K>)key;
}

從上面那個輔助方法可以看到ConcurrentSkipListMap的key必須要能夠進行比較,可以有兩種方式提供比較方法,代碼註釋中已提到。

put(K, V)
public V put(K key, V value) {
   if (value == null)
       throw new NullPointerException();
   // @By Vicky:實際調用內部的doPut方法
   return doPut(key, value, false);
}

/**
* @By Vicky:
 * 三個參數,其中onlyIfAbsent表示是否只在Map中不包含該key的情況下才插入value,默認是false
 */
private V doPut(K kkey, V value, boolean onlyIfAbsent) {
    // 將key封裝成一個Comparable對象,便於直接與其他key進行比較
    Comparable<? super K> key = comparable(kkey);
    for (;;) {
        // 從跳錶中查找最接近指定key的節點:該節點的key小於等於指定key,且處於最底層
        Node<K,V> b = findPredecessor(key);
        Node<K,V> n = b.next;// b的下一個節點,新節點即將插入到b與n之間
        // 準備插入
        for (;;) {
            if (n != null) {// n==null則說明b是鏈表的最後一個節點,則新節點直接插入到鏈表尾部即可
                Node<K,V> f = n.next;// n的下一個節點
                if (n != b.next)               // inconsistent read 此處增加判斷,避免鏈表結構已被修改(針對節點b)
                    break;;
                Object v = n.value;
                if (v == null) {               // n is deleted
                    n.helpDelete(b, f);// 將n從鏈表移除,b和f分別爲n的前繼節點與後繼節點
                    break;
                }
                // 這裏如果v==n說明n是一個刪除標記,用來標記其前繼節點已被刪除,即b已被刪除
                // 查看helpDelete()的註釋
                if (v == n || b.value == null) // b is deleted
                    break;
                // 比較key,此處進行二次比較是避免鏈表已發生改變,比如b後面已被插入一個新的節點
                // (findPredecessor時已經比較過b的next節點(n)的key與指定key的大小,因爲n的key>指定key纔會返回b)
                int c = key.compareTo(n.key);
                if (c > 0) {// 如果指定key>n的key,則判斷下一個節點,直到n==null,或者指定key<n的key
                    b = n;
                    n = f;
                    continue;
                }
                if (c == 0) {// 相等,則更新value即可,更新失敗,就再來一次,一直到成功爲止
                    if (onlyIfAbsent || n.casValue(v, value))
                        return (V)v;
                    else
                        break; // restart if lost race to replace value
                }
                // else c < 0; fall through
            }
            // 創建一個節點,next指向n
            Node<K,V> z = new Node<K,V>(kkey, value, n);
            // 將b的next指向新創建的節點,則新的鏈表爲:b-->new-->n,即將新節點插入到b和n之間
            if (!b.casNext(n, z))
                break;         // restart if lost race to append to b
            // 隨機計算一個層級
            int level = randomLevel();
            // 將z插入到該層級
            if (level > 0)
                insertIndex(z, level);
            return null;
        }
    }
}

代碼中已經附上了大量的註釋,這裏再簡單的梳理下流程。首先put()方法是調用內部的doPut()方法。Comparable< ? super K&> key = comparable(kkey);這一句將key封裝成一個Comparable對象,上面已經介紹了comparable這個方法。接着進入到死循環,循環第一步是調用findPredecessor(key)方法,該方法返回一個key最接近指定key的節點(最接近指的是小於等於),該節點是處於最底層的,下面介紹下這個方法的邏輯。

/** 
* @By Vicky:
 * 在跳錶中查找節點的key小於指定key,且處於最底層的節點,即找到指定key的前繼節點
 * 基本邏輯是從head(跳錶的最高層鏈表的頭結點)開始自右開始查找,當找到該層鏈表的最接近且小於指定key的節點時,往下開始查找,
 * 最終找到最底層的那個節點
 */
private Node<K,V> findPredecessor(Comparable<? super K> key) {
    if (key == null)
        throw new NullPointerException(); // don't postpone errors
    for (;;) {
        Index<K,V> q = head;// head是跳錶的最高層鏈表的頭結點
        Index<K,V> r = q.right;// head的右邊節點
        for (;;) {// 循環
            if (r != null) {// r==null說明該層鏈表已經查找到頭,且未找到符合條件的節點,需開始往下查找
                Node<K,V> n = r.node;// r的數據節點
                K k = n.key;// r的key,用於跟指定key進行比較
                if (n.value == null) { // n的value爲null,說明該節點已被刪除
                    if (!q.unlink(r))// 將該節點從鏈表移除,通過將其(n)前置節點的right指向其(n)的後置節點
                        break;           // restart
                    r = q.right;         // reread r 移除value==null的n節點之後,繼續從n的下一個節點查找
                    continue;
                }
                if (key.compareTo(k) > 0) {// 比較當前查找的節點的key與指定key,如果小於指定key,則繼續查找,
                                           // 大於等於key則q即爲該層鏈表最接近指定key的
                    q = r;
                    r = r.right;
                    continue;
                }
            }
            // 到這裏有兩種情況:1)該層鏈表已經查找完,仍未找到符號條件的節點 2)找到一個符合條件的節點
            // 開始往下一層鏈表進行查找
            Index<K,V> d = q.down;
            if (d != null) {// 從下層對應位置繼續查找
                q = d;
                r = d.right;
            } else// 如果無下層鏈表則直接返回當前節點的node
                return q.node;
        }
    }
}

// @By Vicky:將當前節點的right指向succ的right指向的節點,即將succ從鏈表移除
final boolean unlink(Index<K,V> succ) {
    return !indexesDeletedNode() && casRight(succ, succ.right);
}

該方法的查找邏輯是:從head(跳錶的最高層鏈表的頭結點)開始自右開始查找,當找到該層鏈表的最接近且小於指定key的節點時,往下開始查找,最終找到最底層的那個節點。具體的代碼可以看註釋,應該說的挺明白的了,針對PUT方法,這個方法返回的節點就是將要插入的節點的前繼節點,即新節點將插到該節點後面。下面是查找的示意圖。

findPredecessor查找示意圖:

findPredecessor查找示意圖

findPredecessor()介紹完,我們返回doPut()繼續往下走。通過findPredecessor()返回節點b,獲取b的next節點賦值n,接着進入死循環。判斷n是否爲null,n==null則說明b是鏈表的最後一個節點,則新節點直接插入到鏈表尾部即可,下面我們來看看n!=null的情況。

Node<K,V> f = n.next;// n的下一個節點
if (n != b.next)     // inconsistent read 此處增加判斷,避免鏈表結構已被修改(針對節點b)
   break;;
Object v = n.value;
if (v == null) {               // n is deleted
    n.helpDelete(b, f);// 將n從鏈表移除,b和f分別爲n的前繼節點與後繼節點
    break;
}
// 這裏如果v==n說明n是一個刪除標記,用來標記其前繼節點已被刪除,即b已被刪除
// 查看helpDelete()的註釋
if (v == n || b.value == null) // b is deleted
    break;

這裏首先判斷在這段時間內b的next是否被修改,如果被修改則重新獲取。再接着判斷n和b是否被刪除。這裏說下helpDelete()方法,這個方法比較繞。

void helpDelete(Node<K,V> b, Node<K,V> f) {
    /*
     * Rechecking links and then doing only one of the
     * help-out stages per call tends to minimize CAS
     * interference among helping threads.
     */
    if (f == next && this == b.next) {
        // 判斷當前節點是否已添加刪除標記,未添加則添加刪除標記
        if (f == null || f.value != f) // not already marked
            appendMarker(f);
        else
        // 如果已添加刪除標記,則將b的next指向f的next
        // 因爲當前節點已添加了刪除標記,所以這裏的f只是一個標記:value==本事的節點,其next纔是鏈表的下一個節點
        // 這裏應該是remove方法相關,涉及到ConcurrentSkipListMap的刪除方式
            b.casNext(this, f.next);
    }
}

/**
* @By Vicky:爲當前節點增加一個刪除標記 
 * 將當前節點的next指向一個新節點,該新節點的next指向f,所以從結構是:當前-->new-->f
 * 新節點的value就是他自己,參見Node(Node<K,V> next)構造函數
 * 即刪除標記就是將一個節點與其next節點之間插入一個value就是本事的節點
 */
boolean appendMarker(Node<K,V> f) {
    return casNext(f, new Node<K,V>(f));
}

介紹helpDelete()之前,先簡單介紹ConcurrentSkipListMap是如何刪除一個節點的,其實ConcurrentSkipListMap刪除一個節點現將該節點的value值爲NULL,然後再爲這個節點添加一個刪除標記,但是這個操作有可能失敗,所以如果一個節點的value爲NULL,或者節點有一個刪除標記都被認爲該節點已被刪除。appendMarker()就是用來添加刪除標記,helpDelete()是用來將添加了刪除標記的節點清除。添加標記和如何清除在代碼中的註釋已經說的很清楚了,就不多說了。繼續看doPut()。

// 比較key,此處進行二次比較是避免鏈表已發生改變,比如b後面已被插入一個新的節點
// (findPredecessor時已經比較過b的next節點(n)的key與指定key的大小,因爲n的key>指定key纔會返回b)
int c = key.compareTo(n.key);
if (c > 0) {// 如果指定key>n的key,則判斷下一個節點,直到n==null,或者指定key<n的key
    b = n;
    n = f;
    continue;
}
if (c == 0) {// 相等,則更新value即可,更新失敗,就再來一次,一直到成功爲止
    if (onlyIfAbsent || n.casValue(v, value))
        return (V)v;
    else
        break; // restart if lost race to replace value
}
// else c < 0; fall through

這幾句是考慮當我們找到一個最接近指定key的節點之後有可能鏈表被修改,所以還需要進行二次校驗,從b開始往右邊查找,直至找到一個key大於指定key的節點,那麼新的節點就插入到該節點前面。

// 創建一個節點,next指向n
Node<K,V> z = new Node<K,V>(kkey, value, n);
// 將b的next指向新創建的節點,則新的鏈表爲:b-->new-->n,即將新節點插入到b和n之間
if (!b.casNext(n, z))
    break;         // restart if lost race to append to b
// 隨機計算一個層級
int level = randomLevel();
// 將z插入到該層級
if (level > 0)
    insertIndex(z, level);
return null;

這幾句就是創建一個新的節點,並插入到原鏈表中,所有的修改操作都是使用CAS,只要失敗就會重試,直至成功,所以就算多線程併發操作也不會出現錯誤,而且通過CAS避免了使用鎖,性能比用鎖好很多。


請繼續閱讀後續部分~~~

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