Java併發集合(二)-ConcurrentSkipListMap分析和使用
一、ConcurrentSkipListMap介紹
ConcurrentSkipListMap是執行緒安全的有序的雜湊表,適用於高併發的場景。
ConcurrentSkipListMap和TreeMap,它們雖然都是有序的雜湊表。但是,第一,它們的執行緒安全機制不同,TreeMap是非執行緒安全的,而ConcurrentSkipListMap是執行緒安全的。第二,ConcurrentSkipListMap是通過跳錶實現的,而TreeMap是通過紅黑樹實現的。
在4執行緒1.6萬資料的條件下,ConcurrentHashMap 存取速度是ConcurrentSkipListMap 的4倍左右。
但ConcurrentSkipListMap有幾個ConcurrentHashMap 不能比擬的優點:
1、ConcurrentSkipListMap 的key是有序
2、ConcurrentSkipListMap 支援更高的併發。ConcurrentSkipListMap 的存取時間是log(N),和執行緒數幾乎無關。也就是說在資料量一定的情況下,併發的執行緒越多,ConcurrentSkipListMap越能體現出他的優勢。
在非多執行緒的情況下,應當儘量使用TreeMap。此外對於併發性相對較低的並行程式可以使用Collections.synchronizedSortedMap將TreeMap進行包裝,也可以提供較好的效率。對於高併發程式,應當使用ConcurrentSkipListMap,能夠提供更高的併發度。
所以在多執行緒程式中,如果需要對Map的鍵值進行排序時,請儘量使用ConcurrentSkipListMap,可能得到更好的併發度。
注意,呼叫ConcurrentSkipListMap的size時,由於多個執行緒可以同時對對映表進行操作,所以對映表需要遍歷整個連結串列才能返回元素個數,這個操作是個O(log(n))的操作。
二、ConcurrentSkipListMap資料結構
ConcurrentSkipListMap的資料結構,如下圖所示:
說明:
先以資料“7,14,21,32,37,71,85”序列為例,來對跳錶進行簡單說明。
跳錶分為許多層(level),每一層都可以看作是資料的索引,這些索引的意義就是加快跳錶查詢資料速度。每一層的資料都是有序的,上一層資料是下一層資料的子集,並且第一層(level 1)包含了全部的資料;層次越高,跳躍性越大,包含的資料越少。
跳錶包含一個表頭,它查詢資料時,是從上往下,從左往右進行查詢。現在“需要找出值為32的節點”為例,來對比說明跳錶和普遍的連結串列。
情況1:連結串列中查詢“32”節點
路徑如下圖1-02所示:
需要4步(紅色部分表示路徑)。
情況2:跳錶中查詢“32”節點
路徑如下圖1-03所示:
忽略索引垂直線路上路徑的情況下,只需要2步(紅色部分表示路徑)。
下面說說Java中ConcurrentSkipListMap的資料結構。
(01) ConcurrentSkipListMap繼承於AbstractMap類,也就意味著它是一個雜湊表。
(02) Index是ConcurrentSkipListMap的內部類,它與“跳錶中的索引相對應”。HeadIndex繼承於Index,ConcurrentSkipListMap中含有一個HeadIndex的物件head,head是“跳錶的表頭”。
(03) Index是跳錶中的索引,它包含“右索引的指標(right)”,“下索引的指標(down)”和“雜湊表節點node”。node是Node的物件,Node也是ConcurrentSkipListMap中的內部類。
ConcurrentSkipListMap主要用到了Node和Index兩種節點的儲存方式,通過volatile關鍵字實現了併發的操作
static final class Node<K,V> { final K key; volatile Object value;//value值 volatile Node<K,V> next;//next引用 …… } static class Index<K,V> { final Node<K,V> node; final Index<K,V> down;//downy引用 volatile Index<K,V> righ …… }
三、ConcurrentSkipListMap原始碼分析(JDK1.7.0_40版本)
下面從ConcurrentSkipListMap的新增,刪除,獲取這3個方面對它進行分析。
1. 新增
實際上,put()是通過doPut()將key-value鍵值對新增到ConcurrentSkipListMap中的。
doPut()的原始碼如下:
private V doPut(K kkey, V value, boolean onlyIfAbsent) { Comparable<? super K> key = comparable(kkey); for (;;) { // 找到key的前繼節點 Node<K,V> b = findPredecessor(key); // 設定n為“key的前繼節點的後繼節點”,即n應該是“插入節點”的“後繼節點” Node<K,V> n = b.next; for (;;) { if (n != null) { Node<K,V> f = n.next; // 如果兩次獲得的b.next不是相同的Node,就跳轉到”外層for迴圈“,重新獲得b和n後再遍歷。 if (n != b.next) break; // v是“n的值” Object v = n.value; // 當n的值為null(意味著其它執行緒刪除了n);此時刪除b的下一個節點,然後跳轉到”外層for迴圈“,重新獲得b和n後再遍歷。 if (v == null) { // n is deleted n.helpDelete(b, f); break; } // 如果其它執行緒刪除了b;則跳轉到”外層for迴圈“,重新獲得b和n後再遍歷。 if (v == n || b.value == null) // b is deleted break; // 比較key和n.key int c = key.compareTo(n.key); if (c > 0) { b = n; n = f; continue; } if (c == 0) { if (onlyIfAbsent || n.casValue(v, value)) return (V)v; else break; // restart if lost race to replace value } // else c < 0; fall through } // 新建節點(對應是“要插入的鍵值對”) Node<K,V> z = new Node<K,V>(kkey, value, n); // 設定“b的後繼節點”為z if (!b.casNext(n, z)) break; // 多執行緒情況下,break才可能發生(其它執行緒對b進行了操作) // 隨機獲取一個level // 然後在“第1層”到“第level層”的連結串列中都插入新建節點 int level = randomLevel(); if (level > 0) insertIndex(z, level); return null; } } }
說明:doPut() 的作用就是將鍵值對新增到“跳錶”中。
要想搞清doPut(),首先要弄清楚它的主幹部分 —— 我們先單純的只考慮“單執行緒的情況下,將key-value新增到跳錶中”,即忽略“多執行緒相關的內容”。它的流程如下:
第1步:找到“插入位置”。
即,找到“key的前繼節點(b)”和“key的後繼節點(n)”;key是要插入節點的鍵。
第2步:新建並插入節點。
即,新建節點z(key對應的節點),並將新節點z插入到“跳錶”中(設定“b的後繼節點為z”,“z的後繼節點為n”)。
第3步:更新跳錶。
2. 刪除
實際上,remove()是通過doRemove()將ConcurrentSkipListMap中的key對應的鍵值對刪除的。
doRemove()的原始碼如下:
final V doRemove(Object okey, Object value) { Comparable<? super K> key = comparable(okey); for (;;) { // 找到“key的前繼節點” Node<K,V> b = findPredecessor(key); // 設定n為“b的後繼節點”(即若key存在於“跳錶中”,n就是key對應的節點) Node<K,V> n = b.next; for (;;) { if (n == null) return null; // f是“當前節點n的後繼節點” Node<K,V> f = n.next; // 如果兩次讀取到的“b的後繼節點”不同(其它執行緒操作了該跳錶),則返回到“外層for迴圈”重新遍歷。 if (n != b.next) // inconsistent read break; // 如果“當前節點n的值”變為null(其它執行緒操作了該跳錶),則返回到“外層for迴圈”重新遍歷。 Object v = n.value; if (v == null) { // n is deleted n.helpDelete(b, f); break; } // 如果“前繼節點b”被刪除(其它執行緒操作了該跳錶),則返回到“外層for迴圈”重新遍歷。 if (v == n || b.value == null) // b is deleted break; int c = key.compareTo(n.key); if (c < 0) return null; if (c > 0) { b = n; n = f; continue; } // 以下是c=0的情況 if (value != null && !value.equals(v)) return null; // 設定“當前節點n”的值為null if (!n.casValue(v, null)) break; // 設定“b的後繼節點”為f if (!n.appendMarker(f) || !b.casNext(n, f)) findNode(key); // Retry via findNode else { // 清除“跳錶”中每一層的key節點 findPredecessor(key); // Clean index // 如果“表頭的右索引為空”,則將“跳錶的層次”-1。 if (head.right == null) tryReduceLevel(); } return (V)v; } } }
說明:doRemove()的作用是刪除跳錶中的節點。
和doPut()一樣,我們重點看doRemove()的主幹部分,瞭解主幹部分之後,其餘部分就非常容易理解了。下面是“單執行緒的情況下,刪除跳錶中鍵值對的步驟”:
第1步:找到“被刪除節點的位置”。
即,找到“key的前繼節點(b)”,“key所對應的節點(n)”,“n的後繼節點f”;key是要刪除節點的鍵。
第2步:刪除節點。
即,將“key所對應的節點n”從跳錶中移除 -- 將“b的後繼節點”設為“f”!
第3步:更新跳錶。
3. 獲取
下面以get(Object key)為例,對ConcurrentSkipListMap的獲取方法進行說明。
public V get(Object key) { return doGet(key); }
private V doGet(Object okey) { Comparable<? super K> key = comparable(okey); for (;;) { // 找到“key對應的節點” Node<K,V> n = findNode(key); if (n == null) return null; Object v = n.value; if (v != null) return (V)v; } }
說明:doGet()是通過findNode()找到並返回節點的。
private Node<K,V> findNode(Comparable<? super K> key) { for (;;) { // 找到key的前繼節點 Node<K,V> b = findPredecessor(key); // 設定n為“b的後繼節點”(即若key存在於“跳錶中”,n就是key對應的節點) Node<K,V> n = b.next; for (;;) { // 如果“n為null”,則跳轉中不存在key對應的節點,直接返回null。 if (n == null) return null; Node<K,V> f = n.next; // 如果兩次讀取到的“b的後繼節點”不同(其它執行緒操作了該跳錶),則返回到“外層for迴圈”重新遍歷。 if (n != b.next) // inconsistent read break; Object v = n.value; // 如果“當前節點n的值”變為null(其它執行緒操作了該跳錶),則返回到“外層for迴圈”重新遍歷。 if (v == null) { // n is deleted n.helpDelete(b, f); break; } if (v == n || b.value == null) // b is deleted break; // 若n是當前節點,則返回n。 int c = key.compareTo(n.key); if (c == 0) return n; // 若“節點n的key”小於“key”,則說明跳錶中不存在key對應的節點,返回null if (c < 0) return null; // 若“節點n的key”大於“key”,則更新b和n,繼續查詢。 b = n; n = f; } } }
說明:findNode(key)的作用是在返回跳錶中key對應的節點;存在則返回節點,不存在則返回null。
先弄清函式的主幹部分,即拋開“多執行緒相關內容”,單純的考慮單執行緒情況下,從跳錶獲取節點的演算法。
第1步:找到“被刪除節點的位置”。
根據findPredecessor()定位key所在的層次以及找到key的前繼節點(b),然後找到b的後繼節點n。
第2步:根據“key的前繼節點(b)”和“key的前繼節點的後繼節點(n)”來定位“key對應的節點”。
具體是通過比較“n的鍵值”和“key”的大小。如果相等,則n就是所要查詢的鍵。
四、ConcurrentSkipListMap示例
import java.util.*; import java.util.concurrent.*; /* * ConcurrentSkipListMap是“執行緒安全”的雜湊表,而TreeMap是非執行緒安全的。 * * 下面是“多個執行緒同時操作並且遍歷map”的示例 * (01) 當map是ConcurrentSkipListMap物件時,程式能正常執行。 * (02) 當map是TreeMap物件時,程式會產生ConcurrentModificationException異常。 * * @author skywang */ public class ConcurrentSkipListMapDemo1 { // TODO: map是TreeMap物件時,程式會出錯。 //private static Map<String, String> map = new TreeMap<String, String>(); private static Map<String, String> map = new ConcurrentSkipListMap<String, String>(); public static void main(String[] args) { // 同時啟動兩個執行緒對map進行操作! new MyThread("a").start(); new MyThread("b").start(); } private static void printAll() { String key, value; Iterator iter = map.entrySet().iterator(); while(iter.hasNext()) { Map.Entry entry = (Map.Entry)iter.next(); key = (String)entry.getKey(); value = (String)entry.getValue(); System.out.print("("+key+", "+value+"), "); } System.out.println(); } private static class MyThread extends Thread { MyThread(String name) { super(name); } @Override public void run() { int i = 0; while (i++ < 6) { // “執行緒名” + "序號" String val = Thread.currentThread().getName()+i; map.put(val, "0"); // 通過“Iterator”遍歷map。 printAll(); } } } }
(某一次)執行結果:
(a1, 0), (a1, 0), (b1, 0), (b1, 0), (a1, 0), (b1, 0), (b2, 0), (a1, 0), (a1, 0), (a2, 0), (a2, 0), (b1, 0), (b1, 0), (b2, 0), (b2, 0), (b3, 0), (b3, 0), (a1, 0), (a2, 0), (a3, 0), (a1, 0), (b1, 0), (a2, 0), (b2, 0), (a3, 0), (b3, 0), (b1, 0), (b4, 0), (b2, 0), (a1, 0), (b3, 0), (a2, 0), (b4, 0), (a3, 0), (a1, 0), (a4, 0), (a2, 0), (b1, 0), (a3, 0), (b2, 0), (a4, 0), (b3, 0), (b1, 0), (b4, 0), (b2, 0), (b5, 0), (b3, 0), (a1, 0), (b4, 0), (a2, 0), (b5, 0), (a3, 0), (a1, 0), (a4, 0), (a2, 0), (a5, 0), (a3, 0), (b1, 0), (a4, 0), (b2, 0), (a5, 0), (b3, 0), (b1, 0), (b4, 0), (b2, 0), (b5, 0), (b3, 0), (b6, 0), (b4, 0), (a1, 0), (b5, 0), (a2, 0), (b6, 0), (a3, 0), (a4, 0), (a5, 0), (a6, 0), (b1, 0), (b2, 0), (b3, 0), (b4, 0), (b5, 0), (b6, 0),
結果說明:
示例程式中,啟動兩個執行緒(執行緒a和執行緒b)分別對ConcurrentSkipListMap進行操作。以執行緒a而言,它會先獲取“執行緒名”+“序號”,然後將該字串作為key,將“0”作為value,插入到ConcurrentSkipListMap中;接著,遍歷並輸出ConcurrentSkipListMap中的全部元素。 執行緒b的操作和執行緒a一樣,只不過執行緒b的名字和執行緒a的名字不同。
當map是ConcurrentSkipListMap物件時,程式能正常執行。如果將map改為TreeMap時,程式會產生ConcurrentModificationException異常。
摘抄:
原文:https://blog.csdn.net/vernonzheng/article/details/8244984?utm_source=copy
原文:https://www.cnblogs.com/skywang12345/p/3498556.html