JDK原始碼閱讀(十二) : 基於跳錶的併發容器——ConcurrentSkipListMap
1. 簡介
ConcurrentSkipListMap是有序的hash表,是執行緒安全的。
與之對比的另外兩種hash容器,ConcurrentHashMap雖然是執行緒安全的,但是key並不是有序的;而TreeMap雖然key是有序的,但是不是執行緒安全的。
ConcurrentSkipListMap採用無鎖方案,支援更高的併發,存取時間是O(logN),與執行緒數無關。也就是說,當資料量一定的情況下,併發執行緒數越多,ConcurrentSkipListMap優勢越大。
2. 資料結構
ConcurrentSkipListMap是一種併發的、基於跳錶的Map。
2.1 屬性
(1)Node節點,其中的value域和next域皆採用volatile來實現併發,而key是不可變的,因此用final修飾。
static final class Node<K,V> {
final K key;
volatile Object value;
//指向下一個節點
volatile Node<K,V> next;
Node(K key, Object value, Node<K,V> next) {
this.key = key;
this.value = value;
this.next = next;
}
...
}
(2)index節點,就是跳錶中的索引節點,其中包含了Node節點、指向下邊index節點的指標、指向右邊index節點的指標。
static class Index<K,V> {
final Node<K,V> node;
final Index<K,V> down;
volatile Index<K,V> right;
Index(Node<K,V> node, Index<K,V> down, Index<K,V> right) {
this.node = node;
this.down = down;
this.right = right;
}
...
}
(3)HeadIndex節點是連結串列的頭部索引節點,包含一個level域,表示該跳錶總共有幾層索引。並且其中不儲存有意義的資料。
static final class HeadIndex<K,V> extends Index<K,V> {
final int level;
HeadIndex(Node<K,V> node, Index<K,V> down, Index<K,V> right, int level) {
super(node, down, right);
this.level = level;
}
}
(4)用於標記底層Head的value
private static final Object BASE_HEADER = new Object();
(5)跳錶最頂層的Head
private transient volatile HeadIndex<K,V> head;
(6)用於給key排序的comparator
final Comparator<? super K> comparator;
2.2 構造方法
(1)無參構造器
public ConcurrentSkipListMap() {
//自動排序
this.comparator = null;
initialize();
}
private void initialize() {
keySet = null;
entrySet = null;
values = null;
descendingMap = null;
//建立頂層的head物件,將用於標記底層的BASE_HEADER賦值給該head的Node的value域
head = new HeadIndex<K,V>(new Node<K,V>(null, BASE_HEADER, null),
null, null, 1);
}
(2)構造器中還可以手動傳入comparator,用於自定義排序
public ConcurrentSkipListMap(Comparator<? super K> comparator) {
this.comparator = comparator;
initialize();
}
2.3 新增節點
主要使用put方法來新增鍵值對。注意:ConcurrentSkipListMap中的key和value都不能為null。
public V put(K key, V value) {
//value不允許為null
if (value == null)
throw new NullPointerException();
return doPut(key, value, false);
}
private V doPut(K key, V value, boolean onlyIfAbsent) {
//z將指向要新增的節點
Node<K,V> z;
//key不允許為null
if (key == null)
throw new NullPointerException();
//如果有自定義的比較器
Comparator<? super K> cmp = comparator;
//在迴圈中不斷嘗試新增,直到成功
outer: for (;;) {
//通過索引縮小搜尋範圍,在最底層找到一個比key小的節點,b指向該節點,n指向b的右邊節點
for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) {
//如果n為null,說明沒有後面節點了,可以直接插入到尾部
if (n != null) {
Object v; int c;
//f指向n的右面節點
Node<K,V> f = n.next;
//再次比較n是否等於b的右面節點,如果不等,則說明由於多執行緒問題,存在不一致地讀
//現在需要重啟當前for迴圈,重新執行findPredecessor
if (n != b.next) // inconsistent read
break;
//如果節點n的value為null,則說明節點n已經被刪除
if ((v = n.value) == null) { // n is deleted
//將n徹底刪除,b為n的左邊節點,f為n的右邊節點,只需將b和f相連即可,通過cas方式實現併發
n.helpDelete(b, f);
break;
}
//如果b被刪除了,那也要重啟當前for迴圈,重新執行findPredecessor
if (b.value == null || v == n) // b is deleted
break;
//如果當前key比n的key大,就將b和n都右移一位,然後直接跳到下一輪迴圈
if ((c = cpr(cmp, key, n.key)) > 0) {
b = n;
n = f;
continue;
}
//當key等於n的key時,要將n的key中的value通過cas更新為新的value,並返回舊值
if (c == 0) {
if (onlyIfAbsent || n.casValue(v, value)) {
@SuppressWarnings("unchecked") V vv = (V)v;
return vv;
}
//如果執行緒在cas的無鎖競爭中沒有競爭成功,則重啟當前for迴圈,重新執行findPredecessor
break; // restart if lost race to replace value
}
// else c < 0; fall through
//如果key小於等於n的key,則現在可以插入了
}
//用傳入的key和value構造一個新節點z,z的右面節點為n
z = new Node<K,V>(key, value, n);
//通過cas將b的右面節點設定為z,cas競爭失敗的執行緒則重新開始findPredecessor
if (!b.casNext(n, z))
break; // restart if lost race to append to b
//競爭成功後,跳出outer程式碼塊
break outer;
}
}
//此時,節點已經成功插入。為了保持跳錶的查詢效率,需要隨機的增加索引
//首先,獲取一個隨機數rnd
int rnd = ThreadLocalRandom.nextSecondarySeed();
//讓rnd與0x80000001作與運算,實際上就是判斷rnd的而最高位和最低位是否都是0,
//如果都是0,則會在上層新增一些索引;否則,僅在最低層連結串列中插入節點
if ((rnd & 0x80000001) == 0) { // test highest and lowest bits
int level = 1, max;
//rnd從右邊第2位開始,有幾個連續的1,層數就加幾個1
while (((rnd >>>= 1) & 1) != 0)
++level;
Index<K,V> idx = null;
//h指向最高層的head
HeadIndex<K,V> h = head;
//如果隨機取得的level不超過最高層
if (level <= (max = h.level)) {
//就建立level層索引,從下往上建立一條連結串列。現在只是建立縱向的連線關係,橫向的連線關係還沒建立。
for (int i = 1; i <= level; ++i)
idx = new Index<K,V>(z, idx, null);
}
//如果隨機取得level超過了最高層,就嘗試新增一層索引
else {
level = max + 1; // hold in array and later pick the one to use
//建立level+1個索引陣列
@SuppressWarnings("unchecked")Index<K,V>[] idxs =
(Index<K,V>[])new Index<?,?>[level+1];
//從下到上建立索引節點連結串列,建立縱向的連線關係
for (int i = 1; i <= level; ++i)
idxs[i] = idx = new Index<K,V>(z, idx, null);
for (;;) {
//h指向之前最高層的head
h = head;
//oldLevel表示之前的最高層
int oldLevel = h.level;
//由當前執行緒計算得到的最高層如果小於共享變數的最高層,說明其他執行緒已經增加了層數
if (level <= oldLevel) // 執行緒競爭失敗,重啟findPredecessor方法
break;
//newh先指向之前的最高層
HeadIndex<K,V> newh = h;
//oldbase為head的Node
Node<K,V> oldbase = h.node;
//從oldLevel+1開始到新的最高層level,建立好頭部索引節點,每層headIndex的右側直連該層新建立的index節點
for (int j = oldLevel+1; j <= level; ++j)
newh = new HeadIndex<K,V>(oldbase, newh, idxs[j], j);
//將newh設定為新的最高處head
if (casHead(h, newh)) {
//h指向新的最高層head
h = newh;
//idx指向之前最高層的新建的index
idx = idxs[level = oldLevel];
//退出當前for迴圈
break;
}
}
}
//為所有新建的index節點建立橫向的連線關係,insertionLevel指向當前要插入的層
//從level開始執行橫向插入操作,level = (level <= max ? level : oldLevel)
splice: for (int insertionLevel = level;;) {
//j表示最高層
int j = h.level;
//從最高層開始查詢插入點,當前層找到插入點後,q和r都下移一層繼續查詢
//當q和r都到達要插入的層時,就執行橫向插入操作
for (Index<K,V> q = h, r = q.right, t = idx;;) {
if (q == null || t == null)
break splice;
if (r != null) {
//取得r的Node節點
Node<K,V> n = r.node;
// compare before deletion check avoids needing recheck
//比較key和r的key
int c = cpr(cmp, key, n.key);
//如果節點r被刪除了
if (n.value == null) {
//就將r中橫向連線中刪除,如果執行緒競爭失敗,則重啟
if (!q.unlink(r))
break;
//r重新指向q的右邊節點,然後直接跳到下一輪迴圈
r = q.right;
continue;
}
//如果key大於r中的key,則q和r都要右移一位,直到q和r之間為插入點
if (c > 0) {
q = r;
r = r.right;
continue;
}
}
//如果當前層需要插入
if (j == insertionLevel) {
//嘗試在q和r之間插入t,競爭失敗則重新從最高層開始查詢
if (!q.link(r, t))
break; // restart
//如果要插入的節點已經被刪除,跳出splice程式碼塊
if (t.node.value == null) {
findNode(key);
break splice;
}
//插入層減1,如果到達0時,就跳出splice程式碼塊
if (--insertionLevel == 0)
break splice;
}
//當前層j減1,如果大於等於插入層,並且小於level,就將t下移一位,
//如果等於level,是不能下移的,因為t一開始是指向level那層新建的index
if (--j >= insertionLevel && j < level)
t = t.down;
//q和r也都下移
q = q.down;
r = q.right;
}
}
}
//如果是新插入的節點,就返回null;如果key已經存在,則返回舊的value值
return null;
}
findPredecessor方法在最底層連結串列中,找到最後一個比key小的節點。如果沒有比key小的節點,將返回該層的head節點。
private Node<K,V> findPredecessor(Object key, Comparator<? super K> cmp) {
if (key == null)
throw new NullPointerException(); // don't postpone errors
for (;;) {
//q指向head,r指向q的右邊節點,r節點最終要指向的第一個比key大的節點,q為r的左邊節點
for (Index<K,V> q = head, r = q.right, d;;) {
if (r != null) {
//n為r的node
Node<K,V> n = r.node;
//k為節點n的key
K k = n.key;
//如果n的value為null,則說明節點r已經被刪除
if (n.value == null) {
//從連結串列中解除節點r,即將q連線到r的右邊節點,如果解除失敗,則說明已經有其他執行緒解除了
//執行緒只需要退出當前迴圈,然後重新開始當前迴圈,也就是重新查詢所需節點
if (!q.unlink(r))
break; // restart
//如果本執行緒解除成功,就更新r節點為q.right,繼續當前迴圈的下一輪迴圈來查詢
r = q.right; // reread r
continue;
}
//如果n的value不是null,就比較key和節點r的key,如果key大於節點r的key,
//就要將q和r都右移一位,然後不執行下面程式碼,直接跳到下一輪迴圈。
//當遇到key小於或者等於r的key,就向下執行。
if (cpr(cmp, key, k) > 0) {
//這裡如果有自定義的comparator就用自定義的,如果沒有,就呼叫key自身的compare方法
q = r;
r = r.right;
continue;
}
}
//在當前層,r指向第一個比key大的節點,q指向r的前一個節點
//現在要去當前層的下一層找了,d指向q的下面節點,
//如果沒有下面節點,則說明當前層是最底層,直接返回節點q
if ((d = q.down) == null)
return q.node;
//否則,q指向其下面節點
q = d;
//r指向q的右面節點
r = d.right;
}
}
}
void helpDelete(Node<K,V> b, Node<K,V> f) {
//如果f為當前節點的後繼,b為前驅
if (f == next && this == b.next) {
//如果f為null或者f.value != f,說明還沒進行標記,現在開始標記
if (f == null || f.value != f)
casNext(f, new Node<K,V>(f));
else
//否則,說明已經標記了,就將b的next設定為f的next,清除標記
b.casNext(this, f.next);
}
}
2.4 查詢節點
主要通過get(Key)方法來查詢節點。
public V get(Object key) {
return doGet(key);
}
private V doGet(Object key) {
if (key == null)
throw new NullPointerException();
//獲取比較器
Comparator<? super K> cmp = comparator;
outer: for (;;) {
//呼叫findPredecessor,從高層索引開始一層一層查詢,
//當遇到兩個節點,前一個節點的key比當前key小,後一個節點的key比當前key大,就將前一個節點下移一層,繼續在該層查詢
//直到到達最低層,b指向前一個節點,n指向b的右邊節點,然後在最低層查詢
for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) {
Object v; int c;
//如果後繼節點n為null,則說明沒有找到,跳出迴圈,返回null
if (n == null)
break outer;
//f指向n的右邊節點
Node<K,V> f = n.next;
//再次判斷n是否是b的右邊節點,如果不是,說明有其他執行緒修改了,要重新執行findPredecessor方法
if (n != b.next) // inconsistent read
break;
//如果n被刪除了,就將n從橫向連線中刪除,並重新指向findPredecessor方法
if ((v = n.value) == null) { // n is deleted
n.helpDelete(b, f);
break;
}
//如果b被刪除了,也是要重新執行findPredecessor方法
if (b.value == null || v == n) // b is deleted
break;
//比較key和n的key,如果相同,則說明找到了,直接返回key對應的value
if ((c = cpr(cmp, key, n.key)) == 0) {
@SuppressWarnings("unchecked") V vv = (V)v;
return vv;
}
//如果key小於後續節點的key,說明找不到該節點了,返回null
if (c < 0)
break outer;
//b和n都右移一位
b = n;
n = f;
}
}
return null;
}
2.5 刪除節點
public V remove(Object key) {
return doRemove(key, null);
}
final V doRemove(Object key, Object value) {
if (key == null)
throw new NullPointerException();
Comparator<? super K> cmp = comparator;
outer: for (;;) {
for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) {
Object v; int c;
//沒有後繼節點了,說明找不到相應節點
if (n == null)
break outer;
//f指向n的右邊節點
Node<K,V> f = n.next;
if (n != b.next) // inconsistent read
break;
if ((v = n.value) == null) { // n is deleted
n.helpDelete(b, f);
break;
}
if (b.value == null || v == n) // b is deleted
break;
//如果key小於n的key,則說明找不到該節點了
if ((c = cpr(cmp, key, n.key)) < 0)
break outer;
//如果key大於n的key,就將b和n都右移一位
if (c > 0) {
b = n;
n = f;
continue;
}
//如果key相等,判斷value是否相等,如果不等,說明找不到
if (value != null && !value.equals(v))
break outer;
//將該key的value設定為null,如果競爭失敗,則restart
if (!n.casValue(v, null))
break;
//刪除節點n:
//①首先嚐試給被刪除的節點做一個刪除標記,appendMarker方法見下文
//②如果標記成功,再通過cas嘗試將節點b.next設定為f
if (!n.appendMarker(f) || !b.casNext(n, f))
findNode(key); // retry via findNode
else {
//呼叫findPredecessor可以清除索引,該方法從頂層開始遍歷,
//如果遇到某索引的節點value為null,就會將其從橫向連結串列中移除
findPredecessor(key, cmp);
//清除完索引後,如果頂層沒有索引了,還要減少層數
if (head.right == null)
tryReduceLevel();
}
//返回舊值
@SuppressWarnings("unchecked") V vv = (V)v;
return vv;
}
}
return null;
}
appendMarker方法通過cas嘗試將n的next設定為new Node<K,V>(f)。這是一個特殊的標記節點,與普通Node的區別是,該節點的key為null,value指向自身,同時又包含了連線下一個節點f的指標。
boolean appendMarker(Node<K,V> f) {
return casNext(f, new Node<K,V>(f));
}
Node(Node<K,V> next) {
this.key = null;
this.value = this;
this.next = next;
}
2.6 小結
可以看到,ConcurrentSkipListMap的插入和查詢操作都是無鎖的,完全通過雙重for迴圈來保證執行緒安全。第一層for迴圈為for(;;)的無限迴圈;第二層for迴圈採用各種cas操作,如果執行緒競爭成功,則向下執行;如果競爭失敗,就會回頭重新進入第二層for迴圈。
- 在高併發讀寫環境下,無鎖的ConcurrentSkipListMap效能優於有鎖的ConcurrentHashMap。
- ConcurrentSkipListMap中的key是有序的,而ConcurrentHashMap的key是無序的。
相關文章
- 如何閱讀jdk原始碼?JDK原始碼
- 原始碼閱讀:SDWebImage(十二)——SDWebImageDownloaderOperation原始碼Web
- 基於JDK1.8,Java容器原始碼分析JDKJava原始碼
- JDK原始碼閱讀-String類JDK原始碼
- JDK原始碼閱讀-Object類JDK原始碼Object
- JDK原始碼閱讀-CharSequence介面JDK原始碼
- JDK原始碼閱讀-Comparable介面JDK原始碼
- JDK原始碼閱讀-Integer類JDK原始碼
- JDK原始碼閱讀-Number類JDK原始碼
- JDK原始碼閱讀:String類閱讀筆記JDK原始碼筆記
- JDK原始碼閱讀:Object類閱讀筆記JDK原始碼Object筆記
- Java併發指南14:Java併發容器ConcurrentSkipListMap與CopyOnWriteArrayListJava
- JDK原始碼閱讀(7):ConcurrentHashMap類閱讀筆記JDK原始碼HashMap筆記
- JDK原始碼閱讀(5):HashTable類閱讀筆記JDK原始碼筆記
- JDK原始碼閱讀(4):HashMap類閱讀筆記JDK原始碼HashMap筆記
- JDK1.8 ConcurrentHashMap原始碼閱讀JDKHashMap原始碼
- 原始碼閱讀:AFNetworking(十二)——UIButton+AFNetworking原始碼UI
- Spring原始碼閱讀-IoC容器解析Spring原始碼
- JDK原始碼閱讀(3):AbstractStringBuilder、StringBuffer、StringBuilder類閱讀筆記JDK原始碼UI筆記
- 關於JDK原始碼:我想聊聊如何更高效地閱讀JDK原始碼
- String(JDK1.8)原始碼閱讀記錄JDK原始碼
- 【原始碼閱讀】AndPermission原始碼閱讀原始碼
- spring原始碼閱讀--容器啟動過程Spring原始碼
- Laravel 原始碼閱讀指南 -- 服務容器 IocContainerLaravel原始碼AI
- ThinkPHP6 原始碼閱讀(十二):系統服務PHP原始碼
- JDK1.8原始碼閱讀筆記(1)Object類JDK原始碼筆記Object
- OpenJDK17-JVM原始碼閱讀-ZGC-併發標記JDKJVM原始碼GC
- JDK1.8原始碼分析03之idea搭建原始碼閱讀環境JDK原始碼Idea
- TiDB 原始碼閱讀系列文章(十二)統計資訊(上)TiDB原始碼
- 【原始碼閱讀】Glide原始碼閱讀之with方法(一)原始碼IDE
- 【原始碼閱讀】Glide原始碼閱讀之into方法(三)原始碼IDE
- 詳解Java 容器(第⑤篇)——容器原始碼分析 - 併發容器Java原始碼
- Java原始碼閱讀之TreeMap(紅黑樹) - JDK1.8Java原始碼JDK
- java高併發之ConcurrentSkipListMap的那些事Java
- ConcurrentHashMap基於JDK1.8原始碼剖析HashMapJDK原始碼
- 【原始碼閱讀】Glide原始碼閱讀之load方法(二)原始碼IDE
- Java容器類原始碼分析之Iterator與ListIterator迭代器(基於JDK8)Java原始碼JDK
- Java併發包原始碼學習系列:JDK1.8的ConcurrentHashMap原始碼解析Java原始碼JDKHashMap