JDK1.8的ConcurrentSkipListMap的實現

今天在看redis的基本數據,結構時候,看到有一個跳躍表的數據結構,這之前我一直沒聽說過這種結構,于是先把redis的跳躍表結構的實現看了,然后他的名稱是skiplist,這讓我想起在java8的并發包下有帶skiplist字樣的2個實現類,我才明白,原來java也有這種數據結構,于是先百度了一些關于跳躍表的算法的原理,個人覺得下面這篇文章寫的淺顯易懂,對于入門可謂非常合適。先貼鏈接:http://blog.csdn.net/a1259109679/article/details/46442895。然后順便看看java里面的這種數據結構是怎么實現的。

jdk8下有2個這種實現類,一個是本文介紹的ConcurrentSkipListMap,還有一個是ConcurrentSkipListSet,二者的區別在寫本文之時還未閱讀后者的源碼。

這里需要先明白幾個組成元素:node節點是真正用來存放我們存入的數據的,其中的key和value不必多說,同時還持有一個指向下一個node節點的指針。

index,這個對象有3個屬性,第一個是一個node節點,第二個是down是指向下一個層級的index,right就是指向本層級的下一個index的指針。

頭節點,第一個屬性是代表所在的層級,跳躍表的快速定位依賴與此,然后還有一個指向下一個頭結點的

下圖是一個擁有12個node的分成2層的示意圖,從圖中藍色的線表示一次尋找k7的過程,當然本次數據少,所以優勢并未那么明顯,但是如果數據量大的時候,能大大減少尋找次數。當然這里如果把一排node放在最先會更直觀。

從上圖可以看出,其實整個結構分為2塊,一塊是由我們的node的節點組成的基礎數據,而另外一塊是由一個多層level2組成的數據結構,且尋找過程總是從level最高的開始,然后先找到滿足條件的最后一個index,然后通過index指向下一層對應的index,然后繼續向右向下尋找,直到找到level1且滿足了右邊的node的key比我們需要查找的key大或者為null,然后進入到我們最先的node節點,然后去遍歷鏈表。如下圖所示

對整個結構以及尋找過程清楚以后,我們來看看源碼中怎么樣形成這個結構的。

首先來了解第一個API,put方法。put方法首先驗證value為不為Null,也就是這不允許value為Null,這與map是不同的。真正做加入操作是交給我們的doPut方法操作的。如果度過spring的源碼的同學知道,spring中很多的對外的API也都是做一個必要性驗證,然后真正的操作都交給do**方法操作,而這一的方法一般要么是private的要么是protect的。

下面我們看doput方法。第三個參數是一個布爾值,從名字可以看到應該是一個僅僅在缺席時候加入,這里是不是可以猜測如果已經存在當前key,有2種操作,一種是修改,一種是加入。進來看這里首先驗證了key不能為null,結合上一步的驗證可以知道在我們這個跳躍表里是不允許鍵值為null.然后通過一個findPredecessor,傳入當前新加入的key,和比較的方式來進行尋址。真正的尋找應該放在那里是在這里實現的。

findpredecessor從名字可以理解為尋找前輩,尋找前任。這個尋找過程是從最高level的headIndex開始的,然后先向右開始找,直到右為Null或者,右邊Index的Node的key大于了當前需要加入的key為止,然后再向下尋找,同樣也是直到下為null為止才是出口。通過一系列向右向下尋找,最后會落到我們的需要加入節點的前輩或者前任Node上,然后返回。

尋找前任的過程

然后我們來看看找到這個前輩后我們做什么。首先取得這個前輩的next,如果它的下一個Next節點不為空的時候,我們繼續依著這個next去繼續向下尋找,直到下一個節點為空為止。這里看最后一個if判斷,如果c為0(代表找到了一個key一模一樣的node),然后判斷onlyIfAbsent,一般情況下默認傳入的是false,如果是false,則執行第二個CAS替換,如果為true則不會執行。這驗證了我們前面的猜測。這里有多2個判斷,主要是在并發下的一些判斷吧。能夠完全做到線程安全。

下面看看如果下個節點以后的做法,直接把我們新傳入的key和value包裝成node,然后通過cas來完成next指向,然后就加到了我們的鏈中去了。到目前為止,其實整個加入過程最終操作的是我們的第一塊,也就是整個node鏈條。

下面我們看加入完成后還做了些什么?如果讓你想,你認為接下來會干什么呢?到目前為止,我們整個數據結構是怎么建立的?對,應該到了拆分層次的過程了。在原來的跳躍表原理中拆分解釋了隨機的,也就是擲骰子的方式,這在常理看來是不是有點不靠譜啊,在java中呢?通過源碼可以知道,java中也是隨機的。不過這個隨機是由線程生成的唯一一個數字與一個16禁止數字做與操作后的結果來決定的。看下圖:如果通過判斷為true后,首先將level設置默認為1,然后對rnd進行一個右位移操作后在與1進行與操作后來確定需要分為幾層。具體為什么用這個算法,我看的時候也不大明白,等將來搞明白后在補上。然后通過多次對level++后,就確定了我們需要分成幾層了。確定最終需要的層次后首先判斷,需要分成的level與當前的最大level比較,如果比后者小的話,然后對小于level的每層初始化一個index,每層的node都指向新加入的node,down指向下一層的同樣的自己,右側全部初始化為null。其實也就是如果不需要擴大層的時候,為每層初始化一個這個index,然后準備加入到原來的index鏈條中去

如果需要擴容的話,首先確定的是每次是擴容一層,然后初始化一個對應的indxs的數組,然后為每層都創建包含新加入z的node,并且指向下一層的自己的index。然后通過for循環來進行擴層操作。擴容是從目前最高的那一層開始的,先加一層,包裝一個node指向當前head的node指向的node,第二個參數是down指向當前的head,index指向剛剛創建的對應層次的index,以及最后一個確定層次的編號。這樣就完成了一層中的headindex的創建。然后通過cas吧當前的head與新加入層的head進行替換。這里用語言描述的肯定有些頭暈,我們下面用個圖來講述一下

下圖是一個直觀圖
完成了一個新加入層

但是我們發現完成了新加入,但是對于新加入的Node,只有最頂層建立了指針指向,其他層并未進行加入操作。接下來我們就要完成為每層加入index的操作。由名字可以看到就是插入層次的概念。

此時的h已經指向了我們最新的最高的層的headIndex,然后首先r指向第一個右側index,如果這個index不為null,則先取出這個index的node,然后與當前新加入的key比較,拿到返回值額c,然后判斷node的value不能為null,如果為null則先解除二者之間的關系,解除成功后從新建立right的指向,最后如果新加入的key比當前index的node的key大的話然后順延繼續找下一個index繼續比較,下圖1是橫向的找,找到后進入第二個圖。

從最頂層向右開始順延index找

然后判斷如果當前是最頂層,則把最后的r指向新加入的Index。然后對標志的插入層進行減減操作,然后同時對j(也是當前插入層),進行減減操作,且j必須必代表最高層的level小,然后把t指向了下一層(也就是為新加入的key包裝的index,之前為每一層都建立一個且建立了down指向關系)。然后同樣把headIndex和right同步下移。用文字描述同樣比較蒼白,用一張圖來說明

黑色線代表當前操作,紅色線代表完成建立關系后的操作,都開始指向下一層。

通過上面,就完成了新加入對象的尋址,分層,新關系建立的流程。

下面看remove方法,也很簡單,是通過doremove完成的。

從renmove方法可以看出,即可以通過key移除,也可以通過同時滿足key和value完成。

進來首先也是需要尋找到前key,這里一定是返回的是index所指向的node,也就是上一步一定是僅僅找到index,然后這里通過下面的代碼繼續向下尋找,找到后首先用CAS把value替換為null,然后判斷是不是這層唯一的一個index,如果是的話就把這層給干掉。然后就完成了刪除,從這里看出,它僅僅是把node的value設置為null或者刪除掉本層。

代碼接下面

size()方法,全局并沒有維護一個總數,所以每次都會去遍歷,這里的findFirst也就是直接找到第一個node,然后利用node的next去統計。最后最多能返回Integer.MAX_VALUE.這里在線程并發下是安全的

get方法,也是通過doGet方法完成的。這里不具體列代碼了,也是很簡單,通過findpredecessor方法定位到最近的index,然后繼續順延node去尋找定位返回。

replace方法是可以指定key和value來指定替換,也就是必須滿足value是oldvalue。首先通過findNode找到對應的node。

讀完這里也就完成了整個代碼解讀。但是這里肯定會有疑問,也就是移除操作,其實真正的移除操作都是在其他操作里借用判斷value是不是為null來進行操作的。這里最重要的是helpdelete方法。其實很簡單,就是一個鏈條解除的關系。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容