1. 程式人生 > >Java併發集合(二)-ConcurrentSkipListMap分析和使用

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