在學習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