死磕 java集合之ConcurrentSkipListMap原始碼分析——發現個bug

彤哥讀原始碼發表於2019-04-14

前情提要

點選連結檢視“跳錶”詳細介紹。

拜託,面試別再問我跳錶了!

簡介

跳錶是一個隨機化的資料結構,實質就是一種可以進行二分查詢的有序連結串列

跳錶在原有的有序連結串列上面增加了多級索引,通過索引來實現快速查詢。

跳錶不僅能提高搜尋效能,同時也可以提高插入和刪除操作的效能。

儲存結構

跳錶在原有的有序連結串列上面增加了多級索引,通過索引來實現快速查詢。

skiplist3

原始碼分析

主要內部類

內部類跟儲存結構結合著來看,大概能預測到程式碼的組織方式。

// 資料節點,典型的單連結串列結構
static final class Node<K,V> {
    final K key;
    // 注意:這裡value的型別是Object,而不是V
    // 在刪除元素的時候value會指向當前元素本身
    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;
    }
    
    Node(Node<K,V> next) {
        this.key = null;
        this.value = this; // 當前元素本身(marker)
        this.next = next;
    }
}

// 索引節點,儲存著對應的node值,及向下和向右的索引指標
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;
    }
}

// 頭索引節點,繼承自Index,並擴充套件一個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;
    }
}
複製程式碼

(1)Node,資料節點,儲存資料的節點,典型的單連結串列結構;

(2)Index,索引節點,儲存著對應的node值,及向下和向右的索引指標;

(3)HeadIndex,頭索引節點,繼承自Index,並擴充套件一個level欄位,用於記錄索引的層級;

構造方法


public ConcurrentSkipListMap() {
    this.comparator = null;
    initialize();
}

public ConcurrentSkipListMap(Comparator<? super K> comparator) {
    this.comparator = comparator;
    initialize();
}

public ConcurrentSkipListMap(Map<? extends K, ? extends V> m) {
    this.comparator = null;
    initialize();
    putAll(m);
}

public ConcurrentSkipListMap(SortedMap<K, ? extends V> m) {
    this.comparator = m.comparator();
    initialize();
    buildFromSorted(m);
}
複製程式碼

四個構造方法裡面都呼叫了initialize()這個方法,那麼,這個方法裡面有什麼呢?

private static final Object BASE_HEADER = new Object();

private void initialize() {
    keySet = null;
    entrySet = null;
    values = null;
    descendingMap = null;
    // Node(K key, Object value, Node<K,V> next)
    // HeadIndex(Node<K,V> node, Index<K,V> down, Index<K,V> right, int level)
    head = new HeadIndex<K,V>(new Node<K,V>(null, BASE_HEADER, null),
                              null, null, 1);
}
複製程式碼

可以看到,這裡初始化了一些屬性,並建立了一個頭索引節點,裡面儲存著一個資料節點,這個資料節點的值是空物件,且它的層級是1。

所以,初始化的時候,跳錶中只有一個頭索引節點,層級是1,資料節點是一個空物件,down和right都是null。

ConcurrentSkipList1

通過內部類的結構我們知道,一個頭索引指標包含node, down, right三個指標,為了便於理解,我們把指向node的指標用虛線表示,其它兩個用實線表示,也就是虛線不是表明方向的。

新增元素

通過【拜託,面試別再問我跳錶了!】中的分析,我們知道跳錶插入元素的時候會通過拋硬幣的方式決定出它需要的層級,然後找到各層鏈中它所在的位置,最後通過單連結串列插入的方式把節點及索引插入進去來實現的。

那麼,ConcurrentSkipList中是這麼做的嗎?讓我們一起來探個究竟:

public V put(K key, V value) {
    // 不能儲存value為null的元素
    // 因為value為null標記該元素被刪除(後面會看到)
    if (value == null)
        throw new NullPointerException();

    // 呼叫doPut()方法新增元素
    return doPut(key, value, false);
}

private V doPut(K key, V value, boolean onlyIfAbsent) {
    // 新增元素後儲存在z中
    Node<K,V> z;             // added node
    // key也不能為null
    if (key == null)
        throw new NullPointerException();
    Comparator<? super K> cmp = comparator;

    // Part I:找到目標節點的位置並插入
    // 這裡的目標節點是資料節點,也就是最底層的那條鏈
    // 自旋
    outer: for (;;) {
        // 尋找目標節點之前最近的一個索引對應的資料節點,儲存在b中,b=before
        // 並把b的下一個資料節點儲存在n中,n=next
        // 為了便於描述,我這裡把b叫做當前節點,n叫做下一個節點
        for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) {
            // 如果下一個節點不為空
            // 就拿其key與目標節點的key比較,找到目標節點應該插入的位置
            if (n != null) {
                // v=value,儲存節點value值
                // c=compare,儲存兩個節點比較的大小
                Object v; int c;
                // n的下一個資料節點,也就是b的下一個節點的下一個節點(孫子節點)
                Node<K,V> f = n.next;
                // 如果n不為b的下一個節點
                // 說明有其它執行緒修改了資料,則跳出內層迴圈
                // 也就是回到了外層迴圈自旋的位置,從頭來過
                if (n != b.next)               // inconsistent read
                    break;
                // 如果n的value值為空,說明該節點已刪除,協助刪除節點
                if ((v = n.value) == null) {   // n is deleted
                    // todo 這裡為啥會協助刪除?後面講
                    n.helpDelete(b, f);
                    break;
                }
                // 如果b的值為空或者v等於n,說明b已被刪除
                // 這時候n就是marker節點,那b就是被刪除的那個
                if (b.value == null || v == n) // b is deleted
                    break;
                // 如果目標key與下一個節點的key大
                // 說明目標元素所在的位置還在下一個節點的後面
                if ((c = cpr(cmp, key, n.key)) > 0) {
                    // 就把當前節點往後移一位
                    // 同樣的下一個節點也往後移一位
                    // 再重新檢查新n是否為空,它與目標key的關係
                    b = n;
                    n = f;
                    continue;
                }
                // 如果比較時發現下一個節點的key與目標key相同
                // 說明連結串列中本身就存在目標節點
                if (c == 0) {
                    // 則用新值替換舊值,並返回舊值(onlyIfAbsent=false)
                    if (onlyIfAbsent || n.casValue(v, value)) {
                        @SuppressWarnings("unchecked") V vv = (V)v;
                        return vv;
                    }
                    // 如果替換舊值時失敗,說明其它執行緒先一步修改了值,從頭來過
                    break; // restart if lost race to replace value
                }
                // 如果c<0,就往下走,也就是找到了目標節點的位置
                // else c < 0; fall through
            }

            // 有兩種情況會到這裡
            // 一是到連結串列尾部了,也就是n為null了
            // 二是找到了目標節點的位置,也就是上面的c<0

            // 新建目標節點,並賦值給z
            // 這裡把n作為新節點的next
            // 如果到連結串列尾部了,n為null,這毫無疑問
            // 如果c<0,則n的key比目標key大,相妝於在b和n之間插入目標節點z
            z = new Node<K,V>(key, value, n);
            // 原子更新b的下一個節點為目標節點z
            if (!b.casNext(n, z))
                // 如果更新失敗,說明其它執行緒先一步修改了值,從頭來過
                break;         // restart if lost race to append to b
            // 如果更新成功,跳出自旋狀態
            break outer;
        }
    }

    // 經過Part I,目標節點已經插入到有序連結串列中了

    // Part II:隨機決定是否需要建立索引及其層次,如果需要則建立自上而下的索引

    // 取個隨機數
    int rnd = ThreadLocalRandom.nextSecondarySeed();
    // 0x80000001展開為二進位制為10000000000000000000000000000001
    // 只有兩頭是1
    // 這裡(rnd & 0x80000001) == 0
    // 相當於排除了負數(負數最高位是1),排除了奇數(奇數最低位是1)
    // 只有最高位最低位都不為1的數跟0x80000001做&操作才會為0
    // 也就是正偶數
    if ((rnd & 0x80000001) == 0) { // test highest and lowest bits
        // 預設level為1,也就是隻要到這裡了就會至少建立一層索引
        int level = 1, max;
        // 隨機數從最低位的第二位開始,有幾個連續的1則level就加幾
        // 因為最低位肯定是0,正偶數嘛
        // 比如,1100110,level就加2
        while (((rnd >>>= 1) & 1) != 0)
            ++level;

        // 用於記錄目標節點建立的最高的那層索引節點
        Index<K,V> idx = null;
        // 取頭索引節點(這是最高層的頭索引節點)
        HeadIndex<K,V> h = head;
        // 如果生成的層數小於等於當前最高層的層級
        // 也就是跳錶的高度不會超過現有高度
        if (level <= (max = h.level)) {
            // 從第一層開始建立一條豎直的索引連結串列
            // 這條連結串列使用down指標連線起來
            // 每個索引節點裡面都儲存著目標節點這個資料節點
            // 最後idx儲存的是這條索引連結串列的最高層節點
            for (int i = 1; i <= level; ++i)
                idx = new Index<K,V>(z, idx, null);
        }
        else { // try to grow by one level
            // 如果新的層數超過了現有跳錶的高度
            // 則最多隻增加一層
            // 比如現在只有一層索引,那下一次最多增加到兩層索引,增加多了也沒有意義
            level = max + 1; // hold in array and later pick the one to use
            // idxs用於儲存目標節點建立的豎起索引的所有索引節點
            // 其實這裡直接使用idx這個最高節點也是可以完成的
            // 只是用一個陣列儲存所有節點要方便一些
            // 注意,這裡陣列0號位是沒有使用的
            @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;
                // 舊的最高層級
                int oldLevel = h.level;
                // 再次檢查,如果舊的最高層級已經不比新層級矮了
                // 說明有其它執行緒先一步修改了值,從頭來過
                if (level <= oldLevel) // lost race to add level
                    break;
                // 新的最高層頭索引節點
                HeadIndex<K,V> newh = h;
                // 頭節點指向的資料節點
                Node<K,V> oldbase = h.node;
                // 超出的部分建立新的頭索引節點
                for (int j = oldLevel+1; j <= level; ++j)
                    newh = new HeadIndex<K,V>(oldbase, newh, idxs[j], j);
                // 原子更新頭索引節點
                if (casHead(h, newh)) {
                    // h指向新的最高層頭索引節點
                    h = newh;
                    // 把level賦值為舊的最高層級的
                    // idx指向的不是最高的索引節點了
                    // 而是與舊最高層平齊的索引節點
                    idx = idxs[level = oldLevel];
                    break;
                }
            }
        }

        // 經過上面的步驟,有兩種情況
        // 一是沒有超出高度,新建一條目標節點的索引節點鏈
        // 二是超出了高度,新建一條目標節點的索引節點鏈,同時最高層頭索引節點同樣往上長

        // Part III:將新建的索引節點(包含頭索引節點)與其它索引節點通過右指標連線在一起

        // 這時level是等於舊的最高層級的,自旋
        splice: for (int insertionLevel = level;;) {
            // h為最高頭索引節點
            int j = h.level;

            // 從頭索引節點開始遍歷
            // 為了方便,這裡叫q為當前節點,r為右節點,d為下節點,t為目標節點相應層級的索引
            for (Index<K,V> q = h, r = q.right, t = idx;;) {
                // 如果遍歷到了最右邊,或者最下邊,
                // 也就是遍歷到頭了,則退出外層迴圈
                if (q == null || t == null)
                    break splice;
                // 如果右節點不為空
                if (r != null) {
                    // n是右節點的資料節點,為了方便,這裡直接叫右節點的值
                    Node<K,V> n = r.node;
                    // 比較目標key與右節點的值
                    int c = cpr(cmp, key, n.key);
                    // 如果右節點的值為空了,則表示此節點已刪除
                    if (n.value == null) {
                        // 則把右節點刪除
                        if (!q.unlink(r))
                            // 如果刪除失敗,說明有其它執行緒先一步修改了,從頭來過
                            break;
                        // 刪除成功後重新取右節點
                        r = q.right;
                        continue;
                    }
                    // 如果比較c>0,表示目標節點還要往右
                    if (c > 0) {
                        // 則把當前節點和右節點分別右移
                        q = r;
                        r = r.right;
                        continue;
                    }
                }

                // 到這裡說明已經到當前層級的最右邊了
                // 這裡實際是會先走第二個if

                // 第一個if
                // j與insertionLevel相等了
                // 實際是先走的第二個if,j自減後應該與insertionLevel相等
                if (j == insertionLevel) {
                    // 這裡是真正連右指標的地方
                    if (!q.link(r, t))
                        // 連線失敗,從頭來過
                        break; // restart
                    // t節點的值為空,可能是其它執行緒刪除了這個元素
                    if (t.node.value == null) {
                        // 這裡會去協助刪除元素
                        findNode(key);
                        break splice;
                    }
                    // 當前層級右指標連線完畢,向下移一層繼續連線
                    // 如果移到了最下面一層,則說明都連線完成了,退出外層迴圈
                    if (--insertionLevel == 0)
                        break splice;
                }

                // 第二個if
                // j先自減1,再與兩個level比較
                // j、insertionLevel和t(idx)三者是對應的,都是還未把右指標連好的那個層級
                if (--j >= insertionLevel && j < level)
                    // t往下移
                    t = t.down;

                // 當前層級到最右邊了
                // 那隻能往下一層級去走了
                // 當前節點下移
                // 再取相應的右節點
                q = q.down;
                r = q.right;
            }
        }
    }
    return null;
}

// 尋找目標節點之前最近的一個索引對應的資料節點
private Node<K,V> findPredecessor(Object key, Comparator<? super K> cmp) {
    // key不能為空
    if (key == null)
        throw new NullPointerException(); // don't postpone errors
    // 自旋
    for (;;) {
        // 從最高層頭索引節點開始查詢,先向右,再向下
        // 直到找到目標位置之前的那個索引
        for (Index<K,V> q = head, r = q.right, d;;) {
            // 如果右節點不為空
            if (r != null) {
                // 右節點對應的資料節點,為了方便,我們叫右節點的值
                Node<K,V> n = r.node;
                K k = n.key;
                // 如果右節點的value為空
                // 說明其它執行緒把這個節點標記為刪除了
                // 則協助刪除
                if (n.value == null) {
                    if (!q.unlink(r))
                        // 如果刪除失敗
                        // 說明其它執行緒先刪除了,從頭來過
                        break;           // restart
                    // 刪除之後重新讀取右節點
                    r = q.right;         // reread r
                    continue;
                }
                // 如果目標key比右節點還大,繼續向右尋找
                if (cpr(cmp, key, k) > 0) {
                    // 往右移
                    q = r;
                    // 重新取右節點
                    r = r.right;
                    continue;
                }
                // 如果c<0,說明不能再往右了
            }
            // 到這裡說明當前層級已經到最右了
            // 兩種情況:一是r==null,二是c<0
            // 再從下一級開始找

            // 如果沒有下一級了,就返回這個索引對應的資料節點
            if ((d = q.down) == null)
                return q.node;

            // 往下移
            q = d;
            // 重新取右節點
            r = d.right;
        }
    }
}

// Node.class中的方法,協助刪除元素
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.
     */
    // 這裡的呼叫者this==n,三者關係是b->n->f
    if (f == next && this == b.next) {
        // 將n的值設定為null後,會先把n的下個節點設定為marker節點
        // 這個marker節點的值是它自己
        // 這裡如果不是它自己說明marker失敗了,重新marker
        if (f == null || f.value != f) // not already marked
            casNext(f, new Node<K,V>(f));
        else
            // marker過了,就把b的下個節點指向marker的下個節點
            b.casNext(this, f.next);
    }
}

// Index.class中的方法,刪除succ節點
final boolean unlink(Index<K,V> succ) {
    // 原子更新當前節點指向下一個節點的下一個節點
    // 也就是刪除下一個節點
    return node.value != null && casRight(succ, succ.right);
}

// Index.class中的方法,在當前節點與succ之間插入newSucc節點
final boolean link(Index<K,V> succ, Index<K,V> newSucc) {
    // 在當前節點與下一個節點中間插入一個節點
    Node<K,V> n = node;
    // 新節點指向當前節點的下一個節點
    newSucc.right = succ;
    // 原子更新當前節點的下一個節點指向新節點
    return n.value != null && casRight(succ, newSucc);
}
複製程式碼

我們這裡把整個插入過程分成三個部分:

Part I:找到目標節點的位置並插入

(1)這裡的目標節點是資料節點,也就是最底層的那條鏈;

(2)尋找目標節點之前最近的一個索引對應的資料節點(資料節點都是在最底層的連結串列上);

(3)從這個資料節點開始往後遍歷,直到找到目標節點應該插入的位置;

(4)如果這個位置有元素,就更新其值(onlyIfAbsent=false);

(5)如果這個位置沒有元素,就把目標節點插入;

(6)至此,目標節點已經插入到最底層的資料節點連結串列中了;

Part II:隨機決定是否需要建立索引及其層次,如果需要則建立自上而下的索引

(1)取個隨機數rnd,計算(rnd & 0x80000001);

(2)如果不等於0,結束插入過程,也就是不需要建立索引,返回;

(3)如果等於0,才進入建立索引的過程(只要正偶數才會等於0);

(4)計算while (((rnd >>>= 1) & 1) != 0),決定層級數,level從1開始;

(5)如果算出來的層級不高於現有最高層級,則直接建立一條豎直的索引連結串列(只有down有值),並結束Part II;

(6)如果算出來的層級高於現有最高層級,則新的層級只能比現有最高層級多1;

(7)同樣建立一條豎直的索引連結串列(只有down有值);

(8)將頭索引也向上增加到相應的高度,結束Part II;

(9)也就是說,如果層級不超過現有高度,只建立一條索引鏈,否則還要額外增加頭索引鏈的高度(腦補一下,後面舉例說明);

Part III:將新建的索引節點(包含頭索引節點)與其它索引節點通過右指標連線在一起(補上right指標)

(1)從最高層級的頭索引節點開始,向右遍歷,找到目標索引節點的位置;

(2)如果當前層有目標索引,則把目標索引插入到這個位置,並把目標索引前一個索引向下移一個層級;

(3)如果當前層沒有目標索引,則把目標索引位置前一個索引向下移一個層級;

(4)同樣地,再向右遍歷,尋找新的層級中目標索引的位置,回到第(2)步;

(5)依次迴圈找到所有層級目標索引的位置並把它們插入到橫向的索引連結串列中;

總結起來,一共就是三大步:

(1)插入目標節點到資料節點連結串列中;

(2)建立豎直的down連結串列;

(3)建立橫向的right連結串列;

新增元素舉例

假設初始連結串列是這樣:

ConcurrentSkipList2

假如,我們現在要插入一個元素9。

(1)尋找目標節點之前最近的一個索引對應的資料節點,在這裡也就是找到了5這個資料節點;

(2)從5開始向後遍歷,找到目標節點的位置,也就是在8和12之間;

(3)插入9這個元素,Part I 結束;

ConcurrentSkipList3

然後,計算其索引層級,假如是3,也就是level=3。

(1)建立豎直的down索引連結串列;

(2)超過了現有高度2,還要再增加head索引鏈的高度;

(3)至此,Part II 結束;

ConcurrentSkipList4

最後,把right指標補齊。

(1)從第3層的head往右找當前層級目標索引的位置;

(2)找到就把目標索引和它前面索引的right指標連上,這裡前一個正好是head;

(3)然後前一個索引向下移,這裡就是head下移;

(4)再往右找目標索引的位置;

(5)找到了就把right指標連上,這裡前一個是3的索引;

(6)然後3的索引下移;

(7)再往右找目標索引的位置;

(8)找到了就把right指標連上,這裡前一個是5的索引;

(9)然後5下移,到底了,Part III 結束,整個插入過程結束;

ConcurrentSkipList5

是不是很簡單^^

刪除元素

刪除元素,就是把各層級中對應的元素刪除即可,真的這麼簡單嗎?來讓我們上程式碼:

public V remove(Object key) {
    return doRemove(key, null);
}

final V doRemove(Object key, Object value) {
    // key不為空
    if (key == null)
        throw new NullPointerException();
    Comparator<? super K> cmp = comparator;
    // 自旋
    outer: for (;;) {
        // 尋找目標節點之前的最近的索引節點對應的資料節點
        // 為了方便,這裡叫b為當前節點,n為下一個節點,f為下下個節點
        for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) {
            Object v; int c;
            // 整個連結串列都遍歷完了也沒找到目標節點,退出外層迴圈
            if (n == null)
                break outer;
            // 下下個節點
            Node<K,V> f = n.next;
            // 再次檢查
            // 如果n不是b的下一個節點了
            // 說明有其它執行緒先一步修改了,從頭來過
            if (n != b.next)                    // inconsistent read
                break;
            // 如果下個節點的值奕為null了
            // 說明有其它執行緒標記該元素為刪除狀態了
            if ((v = n.value) == null) {        // n is deleted
                // 協助刪除
                n.helpDelete(b, f);
                break;
            }
            // 如果b的值為空或者v等於n,說明b已被刪除
            // 這時候n就是marker節點,那b就是被刪除的那個
            if (b.value == null || v == n)      // b is deleted
                break;
            // 如果c<0,說明沒找到元素,退出外層迴圈
            if ((c = cpr(cmp, key, n.key)) < 0)
                break outer;
            // 如果c>0,說明還沒找到,繼續向右找
            if (c > 0) {
                // 當前節點往後移
                b = n;
                // 下一個節點往後移
                n = f;
                continue;
            }
            // c=0,說明n就是要找的元素
            // 如果value不為空且不等於找到元素的value,不需要刪除,退出外層迴圈
            if (value != null && !value.equals(v))
                break outer;
            // 如果value為空,或者相等
            // 原子標記n的value值為空
            if (!n.casValue(v, null))
                // 如果刪除失敗,說明其它執行緒先一步修改了,從頭來過
                break;

            // P.S.到了這裡n的值肯定是設定成null了

            // 關鍵!!!!
            // 讓n的下一個節點指向一個market節點
            // 這個market節點的key為null,value為marker自己,next為n的下個節點f
            // 或者讓b的下一個節點指向下下個節點
            // 注意:這裡是或者||,因為兩個CAS不能保證都成功,只能一個一個去嘗試
            // 這裡有兩層意思:
            // 一是如果標記market成功,再嘗試將b的下個節點指向下下個節點,如果第二步失敗了,進入條件,如果成功了就不用進入條件了
            // 二是如果標記market失敗了,直接進入條件
            if (!n.appendMarker(f) || !b.casNext(n, f))
                // 通過findNode()重試刪除(裡面有個helpDelete()方法)
                findNode(key);                  // retry via findNode
            else {
                // 上面兩步操作都成功了,才會進入這裡,不太好理解,上面兩個條件都有非"!"操作
                // 說明節點已經刪除了,通過findPredecessor()方法刪除索引節點
                // findPredecessor()裡面有unlink()操作
                findPredecessor(key, cmp);      // clean index
                // 如果最高層頭索引節點沒有右節點,則跳錶的高度降級
                if (head.right == null)
                    tryReduceLevel();
            }
            // 返回刪除的元素值
            @SuppressWarnings("unchecked") V vv = (V)v;
            return vv;
        }
    }
    return null;
}
複製程式碼

(1)尋找目標節點之前最近的一個索引對應的資料節點(資料節點都是在最底層的連結串列上);

(2)從這個資料節點開始往後遍歷,直到找到目標節點的位置;

(3)如果這個位置沒有元素,直接返回null,表示沒有要刪除的元素;

(4)如果這個位置有元素,先通過n.casValue(v, null)原子更新把其value設定為null;

(5)通過n.appendMarker(f)在當前元素後面新增一個marker元素標記當前元素是要刪除的元素;

(6)通過b.casNext(n, f)嘗試刪除元素;

(7)如果上面兩步中的任意一步失敗了都通過findNode(key)中的n.helpDelete(b, f)再去不斷嘗試刪除;

(8)如果上面兩步都成功了,再通過findPredecessor(key, cmp)中的q.unlink(r)刪除索引節點;

(9)如果head的right指標指向了null,則跳錶高度降級;

刪除元素舉例

假如初始跳錶如下圖所示,我們要刪除9這個元素。

ConcurrentSkipList6

(1)找到9這個資料節點;

(2)把9這個節點的value值設定為null;

(3)在9後面新增一個marker節點,標記9已經刪除了;

(4)讓8指向12;

(5)把索引節點與它前一個索引的right斷開聯絡;

(6)跳錶高度降級;

ConcurrentSkipList7

至於,為什麼要有(2)(3)(4)這麼多步驟呢,因為多執行緒下如果直接讓8指向12,可以其它執行緒先一步在9和12間插入了一個元素10呢,這時候就不對了。

所以這裡搞了三步來保證多執行緒下操作的正確性。

如果第(2)步失敗了,則直接重試;

如果第(3)或(4)步失敗了,因為第(2)步是成功的,則通過helpDelete()不斷重試去刪除;

其實helpDelete()裡面也是不斷地重試(3)和(4);

只有這三步都正確完成了,才能說明這個元素徹底被刪除了。

這一塊結合上面圖中的紅綠藍色好好理解一下,一定要想在併發環境中會怎麼樣。

查詢元素

經過上面的插入和刪除,查詢元素就比較簡單了,直接上程式碼:

public V get(Object key) {
    return doGet(key);
}

private V doGet(Object key) {
    // key不為空
    if (key == null)
        throw new NullPointerException();
    Comparator<? super K> cmp = comparator;
    // 自旋
    outer: for (;;) {
        // 尋找目標節點之前最近的索引對應的資料節點
        // 為了方便,這裡叫b為當前節點,n為下個節點,f為下下個節點
        for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) {
            Object v; int c;
            // 如果連結串列到頭還沒找到元素,則跳出外層迴圈
            if (n == null)
                break outer;
            // 下下個節點
            Node<K,V> f = n.next;
            // 如果不一致讀,從頭來過
            if (n != b.next)                // inconsistent read
                break;
            // 如果n的值為空,說明節點已被其它執行緒標記為刪除
            if ((v = n.value) == null) {    // n is deleted
                // 協助刪除,再重試
                n.helpDelete(b, f);
                break;
            }
            // 如果b的值為空或者v等於n,說明b已被刪除
            // 這時候n就是marker節點,那b就是被刪除的那個
            if (b.value == null || v == n)  // b is deleted
                break;
            // 如果c==0,說明找到了元素,就返回元素值
            if ((c = cpr(cmp, key, n.key)) == 0) {
                @SuppressWarnings("unchecked") V vv = (V)v;
                return vv;
            }
            // 如果c<0,說明沒找到元素
            if (c < 0)
                break outer;
            // 如果c>0,說明還沒找到,繼續尋找
            // 當前節點往後移
            b = n;
            // 下一個節點往後移
            n = f;
        }
    }
    return null;
}
複製程式碼

(1)尋找目標節點之前最近的一個索引對應的資料節點(資料節點都是在最底層的連結串列上);

(2)從這個資料節點開始往後遍歷,直到找到目標節點的位置;

(3)如果這個位置沒有元素,直接返回null,表示沒有找到元素;

(4)如果這個位置有元素,返回元素的value值;

查詢元素舉例

假如有如下圖所示這個跳錶,我們要查詢9這個元素,它走過的路徑是怎樣的呢?可能跟你相像的不一樣。。

ConcurrentSkipList6

(1)尋找目標節點之前最近的一個索引對應的資料節點,這裡就是5;

(2)從5開始往後遍歷,經過8,到9;

(3)找到了返回;

整個路徑如下圖所示:

ConcurrentSkipList8

是不是很操蛋?

為啥不從9的索引直接過來呢?

從我實際打斷點除錯來看確實是按照上圖的路徑來走的。

我猜測可能是因為findPredecessor()這個方法是插入、刪除、查詢元素多個方法共用的,在單連結串列中插入和刪除元素是需要記錄前一個元素的,而查詢並不需要,這裡為了相容三者使得編碼相對簡單一點,所以就使用了同樣的邏輯,而沒有單獨對查詢元素進行優化。

不過也可能是Doug Lea大神不小心寫了個bug,如果有人知道原因請告訴我。(公眾號後臺留言,新公眾號的文章下面不支援留言了,蛋疼)

彩蛋

為什麼Redis選擇使用跳錶而不是紅黑樹來實現有序集合?

請檢視【拜託,面試別再問我跳錶了!】這篇文章。


歡迎關注我的公眾號“彤哥讀原始碼”,檢視更多原始碼系列文章, 與彤哥一起暢遊原始碼的海洋。

qrcode

相關文章