SkipList和java中ConcurrentSkipListMap的實現

SkipList和java中ConcurrentSkipListMap的實現java

簡介

一開始據說SkipList我是一臉懵逼的,啥?還有SkipList?這個是什麼玩意。node

後面通過個人不斷搜索和學習,終於明白了SkipList原來是一種數據結構,而java中的ConcurrentSkipListMap和ConcurrentSkipListSet就是這種結構的實現。數據結構

接下來就讓咱們一步一步的揭開SkipList和ConcurrentSkipListMap的面紗吧。併發

SkipList

先看下維基百科中SkipList的定義:函數

SkipList是一種層級結構。最底層的是排序過的最原始的linked list。學習

往上是一層一層的層級結構,每一個底層節點按照必定的機率出如今上一層list中。這個機率叫作p,一般p取1/2或者1/4。this

先設定一個函數f,能夠隨機產生0和1這兩個數,而且這兩個數出現的概率是同樣的,那麼這時候的p就是1/2。spa

對每一個節點,咱們這樣操做:線程

咱們運行一次f,當f=1時,咱們將該節點插入到上層layer的list中去。當f=0時,不插入。指針

舉個例子,上圖中的list中有10個排序過的節點,第一個節點默認每層都有。對於第二個節點,運行f=0,不插入。對於第三個節點,運行f=1,將第三個節點插入layer 1,以此類推,最後獲得的layer 1 list中的節點有:1,3,4,6,9。

而後咱們再繼續往上構建layer。 最終獲得上圖的SkipList。

經過使用SkipList,咱們構建了多個List,包含不一樣的排序過的節點,從而提高List的查找效率。

咱們經過下圖能有一個更清晰的認識:

每次的查找都是從最頂層開始,由於最頂層的節點數最少,若是要查找的節點在list中的兩個節點中間,則向下移一層繼續查找,最終找到最底層要插入的位置,插入節點,而後再次調用機率函數f,決定是否向上複製節點。

其本質上至關於二分法查找,其查找的時間複雜度是O(logn)。

ConcurrentSkipListMap

ConcurrentSkipListMap是一個併發的SkipList,那麼它具備兩個特色,SkipList和concurrent。咱們分別來說解。

SkipList的實現

上面講解了SkipList的數據結構,接下來看下ConcurrentSkipListMap是怎麼實現這個skipList的:

ConcurrentSkipListMap中有三種結構,base nodes,Head nodes和index nodes。

base nodes組成了有序的鏈表結構,是ConcurrentSkipListMap的最底層實現。

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;
        }
    }
複製代碼

上面能夠看到每一個Node都是一個k,v的entry,而且其有一個next指向下一個節點。

index nodes是構建SkipList上層結構的基本節點:

static class Index<K,V> {
        final Node<K,V> node;
        final Index<K,V> down;
        volatile Index<K,V> right;

        /** * 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;
        }
    }
複製代碼

從上面的構造咱們能夠看到,Index節點包含了Node節點,除此以外,Index還有兩個指針,一個指向同一個layer的下一個節點,一個指向下一層layer的節點。

這樣的結構能夠方便遍歷的實現。

最後看一下HeadIndex,HeadIndex表明的是Head節點:

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;
        }
    }
複製代碼

HeadIndex和Index很相似,只不過多了一個level字段,表示所在的層級。

在ConcurrentSkipListMap初始化的時候,會初始化HeadIndex:

head = new HeadIndex<K,V>(new Node<K,V>(null, BASE_HEADER, null),null, null, 1);
複製代碼

咱們能夠看到HeadIndex中的Node是key=null,value=BASE_HEADER的虛擬節點。初始的level=1。

concurrent的實現

接下來,咱們再看一下併發是怎麼實現的:

基本上併發類都是經過UNSAFE.compareAndSwapObject來實現的,ConcurrentSkipListMap也不例外。

假如咱們有三個節點,b-n-f。如今須要刪除節點n。

第一步,使用CAS將n的valu的值從non-null設置爲null。這個時候,任何外部的操做都會認爲這個節點是不存在的。可是那些內部的插入或者刪除操做仍是會繼續修改n的next指針。

第二步,使用CAS將n的next指針指向一個新的marker節點,從這個時候開始,n的next指針將不會指向任何其餘的節點。

咱們看下marker節點的定義:

Node(Node<K,V> next) {
            this.key = null;
            this.value = this;
            this.next = next;
        }
複製代碼

咱們能夠看到marker節點其實是一個key爲null,value是本身的節點。

第三步,使用CAS將b的next指針指向f。從這一步起,n節點不會再被其餘的程序訪問,這意味着n能夠被垃圾回收了。

咱們思考一下爲何要插入一個marker節點,這是由於咱們在刪除的時候,須要告訴全部的線程,節點n準備被刪除了,由於n原本就指向f節點,這個時候須要一箇中間節點來表示這個準備刪除的狀態。

總結

本文從SkipList數據結構開始,講解了ConcurrentSkipListMap的實現。但願你們可以喜歡。

歡迎關注個人公衆號:程序那些事,更多精彩等着您! 更多內容請訪問:flydean的博客

相關文章
相關標籤/搜索