JDK原始碼閱讀(十二) : 基於跳錶的併發容器——ConcurrentSkipListMap

リュウセイリョウ發表於2020-12-29

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是無序的。

相關文章