HashTable、ConcurrentHashMap實現線程安全對比

HashTable如何實現線程安全

HashTable實現線程安全的方法就是每個方法上加上synchronized關鍵字。

但,HashTable本身是個容器,這也就說明了HashTable本身可以不斷的放大,試想一下,HashTable如果本身如果存在1000個元素,
那麼在get()方法中就會將這1000個元素完全鎖住,期間其他任何線程都得等待。這樣就會造成容器越大,對容器數據操作的效率將越低。

除此之外,包括同步包裝器(Synchronized Wrapper),我們可以調用Collections工具類提供的包裝方法,
來獲取一個同步的包裝容器(如Collections.synchronizedMap),但是它們都是利用非常粗粒度的同步方式,在高併發情況下,性能比較低下。

但是ConcurrentHashMap不同,爲了避免這種情況,它使用了鎖分段技術。

ConcurrentHashMap如何實現線程安全

ConcurrentHashMap是線程安全的。可以在多線程中對ConcurrentHashMap進行操作。
而HashMap線程不安全,ConcurrentHashMap的key和value都不允許爲null。而HashMap則允許。

jdk1.7中用的是鎖分段技術Segment。數據結構是數組+鏈表。
比如容器HashMap中存在1000個元素,各個元素都放置到HashMap數組的鏈表或者紅黑數中,最後得到的數組大小可能只有128,ConcurrentHashMap會根據這128個數組,對其分段,比如以16個數組爲一段,可以分爲8段。在實際獲取元素,添加元素時,會根據元素的索引找到該元素所處的段位,然後只將該段位鎖住,並不影響其他段位的數據操作。
這樣如果按照HashTable的效率爲基本單位來計算,ConcurrentHashMap在jdk1.7及以前的效率會提高8倍,當然數據量越大,提高的效率將越多。

jdk1.8中oncurrentHashMap主要使用了CAS(compareAndSwap)、volatile、synchronized鎖。

跟jdk1.8中的HashMap一樣,數據結構是數組+鏈表+紅黑樹。當鏈表長度過長時,會轉變爲紅黑樹。HashMap數據結構如下:

ConcurrentHashMap依舊使用分段鎖的思想來實現線程安全,不同於jdk1.7及以前,jdk1.8將鎖的粒度更加細分化,以每個數組索引爲鎖來進行實現。
比如HashMap數組中長度有128,那麼就會存在128個鎖將每個索引鎖住。這樣相比於jdk1.7之前在效率上有了很大的改進。

什麼是Unsafe和CAS

ConcurrentHashMap的大部分操作和HashMap是相同的,例如初始化,擴容和鏈表向紅黑樹的轉變等。但是,在ConcurrentHashMap中,大量使用U.compareAndSwapXXX的方法。

這個方法是利用一個CAS算法實現無鎖化的修改值的操作,他可以大大降低鎖代理的性能消耗。

其基本思想就是不斷地去比較當前內存中的變量值與你指定的一個變量值是否相等,如果相等,則接受你指定的修改的值,否則拒絕你的操作。
因爲當前線程中的值已經不是最新的值,你的修改很可能會覆蓋掉其他線程修改的結果。這一點與樂觀鎖,SVN的思想是比較類似的。

static {
    try {
        U = sun.misc.Unsafe.getUnsafe();
        Class<?> k = ConcurrentHashMap.class;
        SIZECTL = U.objectFieldOffset
            (k.getDeclaredField("sizeCtl"));
        TRANSFERINDEX = U.objectFieldOffset
            (k.getDeclaredField("transferIndex"));
        BASECOUNT = U.objectFieldOffset
            (k.getDeclaredField("baseCount"));
        CELLSBUSY = U.objectFieldOffset
            (k.getDeclaredField("cellsBusy"));
        Class<?> ck = CounterCell.class;
        CELLVALUE = U.objectFieldOffset
            (ck.getDeclaredField("value"));
        Class<?> ak = Node[].class;
        ABASE = U.arrayBaseOffset(ak);
        int scale = U.arrayIndexScale(ak);
        if ((scale & (scale - 1)) != 0)
            throw new Error("data type scale not a power of two");
        ASHIFT = 31 - Integer.numberOfLeadingZeros(scale);
    } catch (Exception e) {
        throw new Error(e);
    }
}

同時,在ConcurrentHashMap中還定義了三個原子操作,用於對指定位置的節點進行操作。

這三種原子操作被廣泛的使用在ConcurrentHashMap的get和put等方法中,正是這些原子操作保證了ConcurrentHashMap的線程安全。

// 獲取tab數組的第i個node
static final <K,V> Node<K,V> tabAt(Node<K,V>[] tab, int i) {
    return (Node<K,V>)U.getObjectVolatile(tab, ((long)i << ASHIFT) + ABASE);
}
// 利用CAS算法設置i位置上的node節點。在CAS中,會比較內存中的值與你指定的這個值是否相等,如果相等才接受
static final <K,V> boolean casTabAt(Node<K,V>[] tab, int i,
                                    Node<K,V> c, Node<K,V> v) {
    return U.compareAndSwapObject(tab, ((long)i << ASHIFT) + ABASE, c, v);
}
// 利用volatile方法設置第i個節點的值,這個操作一定是成功的。
static final <K,V> void setTabAt(Node<K,V>[] tab, int i, Node<K,V> v) {
    U.putObjectVolatile(tab, ((long)i << ASHIFT) + ABASE, v);
}

ConcurrentHashMap的put方法

ConcurrentHashMap中最主要的put方法的實現,在put方法中調用了putVal方法,其源碼如下:

final V putVal(K key, V value, boolean onlyIfAbsent) {
    if (key == null || value == null) throw new NullPointerException();
    // 計算hash值
    int hash = spread(key.hashCode());
    int binCount = 0;
    for (Node<K,V>[] tab = table;;) {
        Node<K,V> f; int n, i, fh;
        if (tab == null || (n = tab.length) == 0)
            tab = initTable(); // table是在首次插入元素的時候初始化,lazy
        else if ((f = tabAt(tab, i = (n - 1) & hash)) == null) {
            if (casTabAt(tab, i, null, // 如果這個位置沒有值,直接放進去,由CAS保證線程安全,不需要加鎖
                         new Node<K,V>(hash, key, value, null)))
                break;                   // no lock when adding to empty bin
        }
        else if ((fh = f.hash) == MOVED)
            tab = helpTransfer(tab, f);
        else {
            V oldVal = null;
            synchronized (f) {  // 節點上鎖,這裏的節點可以理解爲hash值相同組成的鏈表的頭節點,鎖的粒度爲頭節點。
                if (tabAt(tab, i) == f) {
                    if (fh >= 0) {
                        binCount = 1;
                        for (Node<K,V> e = f;; ++binCount) {
                            K ek;
                            if (e.hash == hash &&
                                ((ek = e.key) == key ||
                                 (ek != null && key.equals(ek)))) {
                                oldVal = e.val;
                                if (!onlyIfAbsent)
                                    e.val = value;
                                break;
                            }
                            Node<K,V> pred = e;
                            if ((e = e.next) == null) {
                                pred.next = new Node<K,V>(hash, key,
                                                          value, null);
                                break;
                            }
                        }
                    }
                    else if (f instanceof TreeBin) {
                        Node<K,V> p;
                        binCount = 2;
                        if ((p = ((TreeBin<K,V>)f).putTreeVal(hash, key,
                                                       value)) != null) {
                            oldVal = p.val;
                            if (!onlyIfAbsent)
                                p.val = value;
                        }
                    }
                }
            }
            if (binCount != 0) {
                if (binCount >= TREEIFY_THRESHOLD)
                    treeifyBin(tab, i);
                if (oldVal != null)
                    return oldVal;
                break;
            }
        }
    }
    addCount(1L, binCount);
    return null;
}

ConcurrentHashMap的put方法的主要流程如下:

總結

jdk1.8就是將jdk1.7中segment縮小爲1了(表面上)。1.7中Segment的數量由所謂的concurrentcyLevel決定,默認是16,也可以在相應構造函數直接指定。注意,Java需要它是2的冪數值,如果輸入是類似15這種非冪值,會被自動調整到16之類2的冪數值。而1.8中粒度更加細,直接對Node數組中的單個元素進行上鎖。

原文鏈接

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章