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

前情提要

點擊連接查看「跳錶」詳細介紹。java

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

簡介

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

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

跳錶不只能提升搜索性能,同時也能夠提升插入和刪除操做的性能。數組

存儲結構

跳錶在原有的有序鏈表上面增長了多級索引,經過索引來實現快速查找。數據結構

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值,及向下和向右的索引指針;app

(3)HeadIndex,頭索引節點,繼承自Index,並擴展一個level字段,用於記錄索引的層級;dom

構造方法

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
相關文章
相關標籤/搜索