【死磕 Java 集合】— ConcurrentSkipListMap源碼分析

 

【死磕 Java 集合】— ConcurrentSkipListMap源碼分析

 

前情提要

點擊鏈接查看“跳錶”詳細介紹。

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

簡介

跳錶是一個隨機化的數據結構,實質就是一種可以進行二分查找的有序鏈表

跳錶在原有的有序鏈表上面增加了多級索引,通過索引來實現快速查找。

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

存儲結構

跳錶在原有的有序鏈表上面增加了多級索引,通過索引來實現快速查找。

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選擇使用跳錶而不是紅黑樹來實現有序集合?

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

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