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


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM