Java 並發 --- ConcurrentSkipListMap源碼分析__Java

來源:互聯網
上載者:User

在學習ConcurrentSkipListMap 之前 我們需要先來學習一種隨機化的資料結構–跳躍表(skip list)
對於數組的尋找可以有很多方法,如果是有序的,那麼可以採用二分尋找,二分尋找要求元素可以隨機訪問,所以決定了需要把元素儲存在連續記憶體。這樣尋找確實很快,但是插入和刪除元素的時候,為了保證元素的有序性,就需要大量的移動元素了。 對於鏈表而言,不能進行隨機訪問,也就是說不能單純的使用二分尋找,只能通過順序遍曆的方式,為了使鏈表也能有很好的查詢效能,因此才衍生出跳躍表這種資料結構,而且跳躍表相比平衡樹,其實現也相對簡單許多。 跳躍表介紹

跳躍表(skiplist)是一種隨機化的資料結構, 跳躍表以有序的方式在層次化的鏈表中儲存元素, 效率和平衡樹媲美 —— 尋找、刪除、添加等操作都可以在對數期望時間下完成, 並且比起平衡樹來說, 跳躍表的實現要簡單直觀得多,通過下面圖示,可以很直觀的瞭解跳躍表。

上面這張圖就是一個跳躍表的執行個體,先說一下跳躍表的構造特徵: 一個跳躍表應該有若干個層(Level)鏈表組成; 跳躍表中最底層的鏈表包含所有資料,上層鏈表格儲存體的資料的引用; 跳躍表中的資料是有序的; 頭指標(head)指向最高一層的第一個元素;

通過這樣建立多層鏈表的方式,從頂層開始尋找資料,實現了跳躍式的查詢資料,利用空間來換取時間,下面就一起來看看跳躍表的建立以及查詢過程。 跳躍表的尋找

SkipListd的尋找演算法較為簡單,對於上面我們我們要尋找元素42,其過程如下: 比6大,往後找(20),20後面沒有資料了,進入20的下一層 比20大,繼續往後找,找到42.

跳躍表的插入

為什麼說跳躍表是一種隨機化的資料結構呢,那是因為在每次添加資料後,會隨機決定這個資料是否能夠攀升到高層次的鏈表中,也就是同樣的資料構造出來的跳躍表可能是不相同的,這個過程我們看看下面的圖解:
添加資料

添加資料42:
首先會先尋找42應該插入位置的前驅節點,從head 所指節點開始尋找(先向右,再向左),當尋找節點20時,發現後面沒有結點了,那麼進行下一層進行尋找,那麼這樣就會尋找到節點42的前驅應該是30這個結點,然後將資料插入到最底層鏈表中:

然後通過隨機過程確定節點42是否會像上進行攀爬,並且確定攀爬的層次,假設這裡需要向上攀爬兩層,那麼其結果如下所示:
串連索引層

當然這樣並沒有完,現在我們把資料插入到了鏈表中,同時產生了索引層,那麼還有一步,就是串連索引層
跳躍表的刪除

跳躍表的刪除其實和插入差不多,先尋找節點,然後移除每一層的連結即可。

至此對跳躍表應該有了一定的認識了,下面來看看jdk 中ConcurrentSkipListMap 是如何?跳躍表的,結合具體的分析,也可以加強理解。 ConcurrentSkipListMap(jdk 1.8)

ConcurrentSkipListMap 一個並發安全, 基於 skip list 實現有序儲存的Map,ConcurrentSkipListMap提供了三個內部類來構建這樣的鏈表結構:Node、Index、HeadIndex。其中Node表示最底層的單鏈表有序節點、Index表示為基於Node的索引層,HeadIndex用來維護索引層次。 繼承體系

資料結構

Node節點定義:

static final class Node<K,V> {    final K key;    volatile Object value;    volatile Node<K,V> next;    /**     * Creates a new regular node.     */    Node(K key, Object value, Node<K,V> next) {        this.key = key;        this.value = value;        this.next = next;    }    ... //省略部分方法}

Index 定義:

static class Index<K,V> {      final Node<K,V> node;  // 資料節點 引用      final Index<K,V> down; //下層 Index      volatile Index<K,V> right; // 右邊Index      /**       * Creates index node with given values.       */      Index(Node<K,V> node, Index<K,V> down, Index<K,V> right) {          this.node = node;          this.down = down;          this.right = right;      }      ...//省略部分代碼}

HeadIndex 定義:

/** * Nodes heading each level keep track of their level. */static final class HeadIndex<K,V> extends Index<K,V> {    final int level; //索引層,從1開始,Node單鏈表層為0    HeadIndex(Node<K,V> node, Index<K,V> down, Index<K,V> right, int level) {        super(node, down, right);        this.level = level;    }}

HeadIndex 增加一個 level 屬性用來標示索引層級; 注意所有的 HeadIndex 都指向同一個 Base_header 節點;

 /**  * Special value used to identify base-level header  */ private static final Object BASE_HEADER = new Object(); /**  * The topmost head index of the skiplist.  */ private transient volatile HeadIndex<K,V> head; // HeadIndex的頭指標 /**  * The comparator used to maintain order in this map, or null if  * using natural ordering.  (Non-private to simplify access in  * nested classes.)  * @serial  */ final Comparator<? super K> comparator;  //元素比較子

上面是ConcurrentSkipListMap 中跳躍表的定義,但是仍然比較抽象,還是轉換成比較好理解的圖示吧:

注意:在上面的跳躍表的簡單圖示中,沒有畫出HeadIndex結構 構造方法

1、預設構造

/** * Constructs a new, empty map, sorted according to the * {@linkplain Comparable natural ordering} of the keys. */public ConcurrentSkipListMap() {    this.comparator = null;    initialize();}

2、指定比較子

/** * Constructs a new, empty map, sorted according to the specified * comparator. */public ConcurrentSkipListMap(Comparator<? super K> comparator) {    this.comparator = comparator;    initialize();}

3、通過集合初始化

/** * Constructs a new map containing the same mappings as the given map, * sorted according to the {@linkplain Comparable natural ordering} of * the keys. */public ConcurrentSkipListMap(Map<? extends K, ? extends V> m) {    this.comparator = null;    initialize();    putAll(m);}/** * Constructs a new map containing the same mappings and using the * same ordering as the specified sorted map. */public ConcurrentSkipListMap(SortedMap<K, ? extends V> m) {    this.comparator = m.comparator();    initialize();    buildFromSorted(m);}

構造方法中都調用了initialize() 方法

private void initialize() {    keySet = null;    entrySet = null;    values = null;    descendingMap = null;    //初始化head    head = new HeadIndex<K,V>(new Node<K,V>(null, BASE_HEADER, null),                              null, null, 1);}

初始建立的跳躍表結構如下:
添加資料(put)

public V put(K key, V value) {    if (value == null)        throw new NullPointerException();    return doPut(key, value, false);}

要求value 不可為空,內部調用doPut 方法。 findPredecessor 方法

findPredecessor 方法的功能是尋找某個key的前驅(如果遇到需要刪除的節點,那麼進行輔助刪除)。從最高層的headIndex開始向右一步一步比較,直到right為null或者右邊節點的Node的key大於當前key為止,然後再向下尋找,依次重複該過程,直到down為null為止,即找到了前驅。

private Node<K,V> findPredecessor(Object key, Comparator<? super K> cmp) {    if (key == null)        throw new NullPointerException(); // don't postpone errors    for (;;) {        // 從head 開始遍曆        for (Index<K,V> q = head, r = q.right, d;;) {             // r != null,表示該節點右邊還有節點,需要進行比較            if (r != null) {                Node<K,V> n = r.node;                K k = n.key;                // value == null,表示該節點已經被刪除了                // 通過unlink()刪除該節點                if (n.value == null) {                    if (!q.unlink(r))                        break;           // restart                    r = q.right;         // reread r                    continue;                }                 // 如果key 大於r節點的key 則繼續向後遍曆                if (cpr(cmp, key, k) > 0) {                    q = r;                    r = r.right;                    continue;                }            }            //如果dowm == null,表示指標已經達到最下層了,直接返回該節點            if ((d = q.down) == null)                return q.node;            //否則進入下層尋找               q = d;            r = d.right;        }    }}
doPut 方法
private V doPut(K key, V value, boolean onlyIfAbsent) {    Node<K,V> z;             // added node    if (key == null)        throw new NullPointerException();    Comparator<? super K> cmp = comparator;    outer: for (;;) {        // b 為前繼節點, n是前繼節點的next        for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) {            if (n != null) {                //b 是鏈表的最後一個節點                Object v; int c;                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;                }                if (b.value == null || v == n) // b is deleted                    break;                 // 節點大於,往後繼續尋找                  if ((c = cpr(cmp, key, n.key)) > 0) {                    b = n;                    n = f;                    continue;                }                //相等,根據參數onlyIfAbsent  決定是否覆蓋value                if (c == 0) {                    if (onlyIfAbsent || n.casValue(v, value)) {                        @SuppressWarnings("unchecked") V vv = (V)v;                        return vv;                    }                    // 競爭失敗 重來                    break; // restart if lost race to replace value                }                // else c < 0; fall through            }            //建立節點            z = new Node<K,V>(key, value, n);            //插入節點,如果失敗,則重來            if (!b.casNext(n, z))                break;         // restart if lost race to append to b            break outer;        }    }    //隨機數    int rnd = ThreadLocalRandom.nextSecondarySeed();    // 判斷是否需要添加level    if ((rnd & 0x80000001) == 0) { // test highest and lowest bits        int level = 1, max;        //擷取 level        while (((rnd >>>= 1) & 1) != 0)            ++level;        Index<K,V> idx = null;        HeadIndex<K,V> h = head;        //如果層次level大於最大的層次話則需要新增一層,否則就在相應層次以及小於該level的層次進行節點新增處理。        // level比最高層次head.level小,直接產生需要的index        if (level <= (max = h.level)) {            for (int i = 1; i <= level; ++i) //產生index                idx = new Index<K,V>(z, idx, null);        }        else { // level > max             level = max + 1; // hold in array and later pick the one to use            @SuppressWarnings("unchecked")Index<K,V>[] idxs =                (Index<K,V>[])new Index<?,?>[level+1];            for (int i = 1; i <= level; ++i) //產生index                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;                // 產生新的HeadIndex節點                for (int j = oldLevel+1; j <= level; ++j)                    newh = new HeadIndex<K,V>(oldbase, newh, idxs[j], j);                //更新head                    if (casHead(h, newh)) {                    h = newh;                    idx = idxs[level = oldLevel];                    break;                }            }        }        /**         *前面產生了索引層,但是並沒有將這些Index插入到相應的層次當中         *下面的代碼就是將index連結到相對應的層當中         */        // 從插入的層次level開始        splice: for (int insertionLevel = level;;) {            int j = h.level;             //從headIndex開始            for (Index<K,V> q = h, r = q.right, t = idx;;) {                if (q == null || t == null)                    break splice;                // r != null;這裡是找到相應層次的插入節點位置,注意這裡只橫向找                    if (r != null) {                    Node<K,V> n = r.node;                    // compare before deletion check avoids needing recheck                    int c = cpr(cmp, key, n.key);                    //需要刪除r                    if (n.value == null) {                        if (!q.unlink(r))                            break;                        r = q.right; //繼續向後                        continue;                    }                    //向右進行遍曆                    if (c > 0) {                        q = r;                        r = r.right;                        continue;                    }                }                // 上面找到節點要插入的位置,這裡就插入                if (j == insertionLevel) {                    //建立連結,失敗重試                    if (!q.link(r, t))                        break; // restart                    if (t.node.value == null) {                        findNode(key);// 尋找節點,尋找過程中會刪除需要刪除的節點                        break splice;                    }                    //連結完畢                    if (--insertionLevel == 0)                        break splice;                }                //向下繼續連結其它index 層                if (--j >= insertionLevel && j < level)                    t = t.down;                q = q.down;                r = q.right;            }        }    }    return null;}

doPut方法代碼比較長,來梳理一下邏輯:
1、通過 findPredecessor()方法確認key要插入的位置
2、進行資料校正,如果發現需要刪除節點,則進行輔助刪除,如果其他線程改變了跳躍表,則進行重試或遍曆尋找合適的位置。
3、如果跳躍表中已經存在該key,則根據onlyIfAbsent 確定是否覆蓋舊值。
4、產生節點,插入到最底層的資料鏈表中。
5、根據隨機值確定是否建立索引層,如果不需要則返回,否則執行第6步
6、如果需要建立的索引層超過最大的level,則需要建立HeadIndex 索引層,否則只需要建立Index 索引層即可。
7、從head 開始進行遍曆,將每一層的新添加的Index索引層進行串連(這個可以結合上面跳躍表串連索引層圖示來理解)。 尋找資料

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 方法尋找其前驅節點        for (Node<K,V> b = findPredecessor(key, cmp), n = b.next;;) {            Object v; int c;            if (n == null) //不存在該key                break outer;            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;            }            //b會被刪除,重來            if (b.value == null || v == n)  // b is deleted                break;            // 找到了                if ((c = cpr(cmp, key, n.key)) == 0) {                @SuppressWarnings("unchecked") V vv = (V)v;                return vv;            }            if (c < 0) //不存在該key                break outer;            // 其它線程添加了資料,向後繼續遍曆尋找                b = n;            n = f;        }    }    return null;}

尋找資料比較簡單,先通過findPredecessor 尋找其前驅,然後順著right一直往右找即可,同時在這個過程中,如果發現某個節點需要刪除,則需要進行輔助刪除,如果發現跳躍表資料結構被其它線程改變,會重新嘗試擷取其前驅。 刪除資料

ConcurrentSkipListMap 支援並行作業,因此在刪除的時候需要注意,因為在刪除的同時,其它線程可能在該位置上進行資料插入,這樣很容易造成資料的丟失,這個我們在前面的阻塞隊列SynchronousQueue 也遇到類似的問題,在SynchronousQueue 中用了一個cleanMe標記需要刪除的節點的前驅,在ConcurrentSkipListMap 中也是類似的機制,會在刪除後面添加一個特殊的節點進行標記,然後再進行整體的刪除,如果不進行標記,那麼如果正在刪除的節點,可能其它線程正在此節點後面添加資料,造成資料丟失.

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

調用doRemove()方法,doRemove有兩個參數,一個是key,另外一個是value,所以doRemove方法即提供remove key,也提供同時滿足key-value。

//指定key 和value刪除相應的節點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) // 不存在該key                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即將也被刪除            if (b.value == null || v == n)      // b is deleted                break;            // 不存在該key                if ((c = cpr(cmp, key, n.key)) < 0)                break outer;            if (c > 0) { //向後繼續遍曆尋找                b = n;                n = f;                continue;            }            //key 相等,value 不相等,退出            if (value != null && !value.equals(v))                break outer;            //邏輯刪除,設定value=null                if (!n.casValue(v, null))                 break;            //先添加刪除標記,然後再進行刪除操作                if (!n.appendMarker(f) || !b.casNext(n, f))                findNode(key);                  // retry via findNode            else {                // 清除索引層                findPredecessor(key, cmp);      // clean index                 //該層已經沒有節點,刪掉該層                if (head.right == null)                    tryReduceLevel();            }            @S

聯繫我們

該頁面正文內容均來源於網絡整理,並不代表阿里雲官方的觀點,該頁面所提到的產品和服務也與阿里云無關,如果該頁面內容對您造成了困擾,歡迎寫郵件給我們,收到郵件我們將在5個工作日內處理。

如果您發現本社區中有涉嫌抄襲的內容,歡迎發送郵件至: info-contact@alibabacloud.com 進行舉報並提供相關證據,工作人員會在 5 個工作天內聯絡您,一經查實,本站將立刻刪除涉嫌侵權內容。

A Free Trial That Lets You Build Big!

Start building with 50+ products and up to 12 months usage for Elastic Compute Service

  • Sales Support

    1 on 1 presale consultation

  • After-Sales Support

    24/7 Technical Support 6 Free Tickets per Quarter Faster Response

  • Alibaba Cloud offers highly flexible support services tailored to meet your exact needs.