更多文章請閱讀: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在原始鏈表的基礎上增加了跳錶的結構,所以需要兩個額外的內部類來封裝鏈表的節點,以及跳錶的節點——Node和Index。
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()介紹完,我們返回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避免了使用鎖,性能比用鎖好很多。