跳表(SkipList) 和 ConcurrentSkipListMap


一、跳表(SkipList)

對於單鏈表,即使鏈表是有序的,如果想要在其中查找某個數據,也只能從頭到尾遍歷鏈表,這樣效率自然就會很低,跳表就不一樣了。跳表是一種可以用來快速查找的數據結構,有點類似於平衡樹。它們都可以對元素進行快速的查找。但一個重要的區別是:對平衡樹的插入和刪除往往很可能導致平衡樹進行一次全局的調整;而對跳表的插入和刪除,只需要對整個數據結構的局部進行操作即可。這樣帶來的好處是:在高並發的情況下,需要一個全局鎖,來保證整個平衡樹的線程安全;而對於跳表,則只需要部分鎖即可。這樣,在高並發環境下,就可以擁有更好的性能。就查詢的性能而言,跳表的時間復雜度是 O(logn)。

跳表的本質,其實是同時維護了多個鏈表,並且鏈表是分層的:

其中最低層的鏈表,維護了跳表內所有的元素,每往上一層鏈表,都是下面一層的子集。

跳表內每一層鏈表的元素都是有序的。查找時,可以從頂級鏈表開始找。一旦發現被查找的元素大於當前鏈表中的取值,就會轉入下一層鏈表繼續查找。也就是說在查找過程中,搜索是跳躍式的。如上圖所示,在跳表中查找元素18:

可以看到,在查找 18 的時候,原來需要遍歷 12 次,現在只需要 7 次即可。針對鏈表長度比較大的時候,構建索引,查找效率的提升就會非常明顯。

從上面很容易看出,跳表是一種利用空間換時間的算法

二、ConcurrentSkipListMap

ConcurrentSkipListMap 是一個線程安全的基於跳躍表實現的非阻塞的 Map,它要求 Map 中的 key 和 value 都不能為 null。相較於哈希實現的 Map,跳表內的所有元素都是有序的;相較於紅黑樹結構 treeMap,ConcurrentSkipListMap 是線程安全的。

ConcurrentSkipListMap 適用於高並發的場景,在數據量一定的情況下,並發的線程越多,ConcurrentSkipListMap 越能體現出他查詢的優勢。

ConcurrentSkipListMap 的存取性能遜色於 ConcurrentHashMap(在 4 線程 1.6 萬數據的條件下,ConcurrentHashMap 存取速度是 ConcurrentSkipListMap 的 4倍左右),它的優勢在於跳表內的所有元素都是有序的。

在非多線程的情況下,應當盡量使用 TreeMap。此外對於並發性相對較低的並行程序可以使用 Collections.synchronizedSortedMap 將 TreeMap 進行包裝,也可以保證線程安全。

三、ConcurrentSkipListMap 數據結構

從源碼可以分析得到 ConcurrentSkipListMap 的整個數據結構如下:

來分別看看 HeadIndex、Index 和 Node 的類信息:

    static class Index<K,V> {
        final Node<K,V> node;
        final Index<K,V> down;
        volatile Index<K,V> right;
    }
    static final class HeadIndex<K,V> extends Index<K,V> {
        final int level;
        HeadIndex(Node<K,V> node, Index<K,V> down, Index<K,V> right, int level) {
            super(node, down, right);
            this.level = level;
        }
    }
    static final class Node<K,V> {
        final K key;
        volatile Object value;
        volatile Node<K,V> next;
    }

可以看到,Index 包含了 Node 的引用,並用 right 和 down 引用分別指向各自的 Index 域;HeadIndex 繼承自 Index,作為索引的頭節點,維護了跳表中 level 的概念;Node 節點存儲了實際的 key、value 信息,並用 next 引用構建單鏈表。

具體的源碼分析可以參見這篇文章:https://www.jianshu.com/p/2075a76a43a3

四、ConcurrentSkipListMap 示例

下面是 “多個線程同時操作並且遍歷 map” 的示例,以驗證 ConcurrentSkipListMap 的線程安全:

  1. 當 map 是 ConcurrentSkipListMap 對象時,程序能正常運行。
  2. 當 map 是 TreeMap 對象時,程序會產生 ConcurrentModificationException 異常。
public class ConcurrentSkipListMapTest {

    //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 iterator = MAP.entrySet().iterator();
        while (iterator.hasNext()) {
            Map.Entry entry = (Map.Entry) iterator.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");
                printAll();
            }
        }
    }
}

五、ConcurrentSkipListSet

Java 中所有 Set 幾乎都是內部用一個 Map 來實現, 因為 Map 里的 KeySet() 就是一個 Set 集合,而 value 是假值,全部使用同一個 Object 即可。

ConcurrentSkipListSet 也不例外,它內部使用 ConcurrentSkipListMap 集合實現,並利用其 addIfAbsent() 方法實現元素去重。


免責聲明!

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



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