Java編程的邏輯 (75) - 併發容器 - 基於SkipList的Map和Set

本系列文章經補充和完善,已修訂整理成書《Java編程的邏輯》,由機械工業出版社華章分社出版,於2018年1月上市熱銷,讀者好評如潮!各大網店和書店有售,歡迎購買,京東自營連接http://item.jd.com/12299018.htmlhtml


上節咱們介紹了ConcurrentHashMap,ConcurrentHashMap不能排序,容器類中能夠排序的Map和Set是TreeMapTreeSet但它們不是線程安全的。Java併發包中與TreeMap/TreeSet對應的併發版本是ConcurrentSkipListMap和ConcurrentSkipListSet,本節,咱們就來簡要探討這兩個類。git

基本概念github

咱們知道,TreeSet是基於TreeMap實現的,與此相似,ConcurrentSkipListSet也是基於ConcurrentSkipListMap實現的,因此,咱們主要來探討ConcurrentSkipListMap。算法

ConcurrentSkipListMap是基於SkipList實現的,SkipList稱爲跳躍表或跳錶,是一種數據結構,待會咱們會進一步介紹。併發版本爲何採用跳錶而不是樹呢?緣由也很簡單,由於跳錶更易於實現高效併發算法。編程

ConcurrentSkipListMap有以下特色:swift

  • 沒有使用鎖,全部操做都是無阻塞的,全部操做均可以並行,包括寫,多個線程能夠同時寫。
  • 與ConcurrentHashMap相似,迭代器不會拋出ConcurrentModificationException,是弱一致的,迭代可能反映最新修改也可能不反映,一些方法如putAll, clear不是原子的。
  • 與ConcurrentHashMap相似,一樣實現了ConcurrentMap接口,直接支持一些原子複合操做。
  • 與TreeMap同樣,可排序,默認按鍵天然有序,能夠傳遞比較器自定義排序,實現了SortedMap和NavigableMap接口。

看段簡單的使用代碼:數組

public static void main(String[] args) {
    Map<String, String> map = new ConcurrentSkipListMap<>(
            Collections.reverseOrder());
    map.put("a", "abstract");
    map.put("c", "call");
    map.put("b", "basic");
    System.out.println(map.toString());
}

程序輸出爲:安全

{c=call, b=basic, a=abstract}

表示是有序的。微信

ConcurrentSkipListMap的大部分方法,咱們以前都有介紹過,有序的方法,與TreeMap是相似的,原子複合操做,與ConcurrentHashMap是相似的,因此咱們就不贅述了。數據結構

須要說明一下的是它的size方法,與大多數容器實現不一樣,這個方法不是常量操做,它須要遍歷全部元素,複雜度爲O(N),並且遍歷結束後,元素個數可能已經變了,通常而言,在併發應用中,這個方法用處不大。

下面咱們主要介紹下其基本實現原理。

基本實現原理

咱們先來介紹下跳錶的結構,跳錶是基於鏈表的,在鏈表的基礎上加了多層索引結構。咱們經過一個簡單的例子來看下,假定容器中包含以下元素:

3, 6, 7, 9, 12, 17, 19, 21, 25, 26

對Map來講,這些值能夠視爲鍵。ConcurrentSkipListMap會構造相似下圖所示的跳錶結構:

最下面一層,就是最基本的單向鏈表,這個鏈表是有序的。雖然是有序的,但咱們知道,與數組不一樣,鏈表不能根據索引直接定位,不能進行二分查找。

爲了快速查找,跳錶有多層索引結構,這個例子中有兩層,第一層有5個節點,第二層有2個節點。高層的索引節點必定同時是低層的索引節點,好比9和21。

高層的索引節點少,低層的多,統計機率上,第一層索引節點是實際元素數的1/2,第二層是第一層的1/2,逐層減半,但這不是絕對的,有隨機性,只是大概如此。

對於每一個索引節點,有兩個指針,一個向右,指向下一個同層的索引節點,另外一個向下,指向下一層的索引節點或基本鏈表節點。

有了這個結構,就能夠實現相似二分查找了,查找元素老是從最高層開始,將待查值與下一個索引節點的值進行比較,若是大於索引節點,就向右移動,繼續比較,若是小於,則向下移動到下一層進行比較。

下圖兩條線展現了查找值19和8的過程:

對於19,查找過程是:

  1. 與9相比,大於9
  2. 向右與21相比,小於21
  3. 向下與17相比,大於17
  4. 向右與21相比,小於21
  5. 向下與19相比,找到

對於8,查找過程是:

  1. 與9相比,小於9
  2. 向下與6相比,大於6
  3. 向右與9相比,小於9
  4. 向下與7相比,大於7
  5. 向右與9相比,小於9,不能再向下,沒找到

這個結構是有序的,查找的性能與二叉樹相似,複雜度是O(log(N)),不過,這個結構是如何構建起來的呢?

與二叉樹相似,這個結構是在更新過程當中進行保持的,保存元素的基本思路是:

  1. 先保存到基本鏈表,找到待插入的位置,找到位置後,先插入基本鏈表
  2. 更新索引層。

對於索引更新,隨機計算一個數,表示爲該元素最高建幾層索引,一層的機率爲1/2,二層爲1/4,三層爲1/8,依次類推。而後從最高層到最低層,在每一層,爲該元素創建索引節點,建的過程也是先查找位置,再插入。

對於刪除元素,ConcurrentSkipListMap不是一會兒真的進行刪除,爲了不併發衝突,有一個複雜的標記過程,在內部遍歷元素的過程當中會真正刪除。

以上咱們只是介紹了基本思路,爲了實現併發安全、高效、無鎖非阻塞,ConcurrentSkipListMap的實現很是複雜,具體咱們就不探討了,感興趣的讀者能夠參考其源碼,其中提到了多篇學術論文,論文中描述了它參考的一些算法。

對於常見的操做,如get/put/remove/containsKey,ConcurrentSkipListMap的複雜度都是O(log(N))。

上面介紹的SkipList結構是爲了便於併發操做的,若是不須要併發,可使用另外一種更爲高效的結構,數據和全部層的索引放到一個節點中,以下圖所示:

 

對於一個元素,只有一個節點,只是每一個節點的索引個數可能不一樣,在新建一個節點時,使用隨機算法決定它的索引個數,平均而言,1/2的元素有兩個索引,1/4的元素有三個索引,依次類推。

小結

本節簡要介紹了ConcurrentSkipListMap和ConcurrentSkipListSet,它們基於跳錶實現,有序,無鎖非阻塞,徹底並行,主要操做複雜度爲O(log(N))。

下一節,咱們來探討併發隊列。 

(與其餘章節同樣,本節全部代碼位於 https://github.com/swiftma/program-logic)

----------------

未完待續,查看最新文章,敬請關注微信公衆號「老馬說編程」(掃描下方二維碼),從入門到高級,深刻淺出,老馬和你一塊兒探索Java編程及計算機技術的本質。用心原創,保留全部版權。

相關文章
相關標籤/搜索