2種辦法讓HashMap線程安全

HashMap不是線程安全的,往往在寫程序時需要通過一些方法來回避.其實JDK原生的提供了2種方法讓HashMap支持線程安全.

方法一:通過Collections.synchronizedMap()返回一個新的Map,這個新的map就是線程安全的. 這個要求大家習慣基於接口編程,因爲返回的並不是HashMap,而是一個Map的實現.

方法二:重新改寫了HashMap,具體的可以查看java.util.concurrent.ConcurrentHashMap. 這個方法比方法一有了很大的改進.

下面對這2中實現方法從各個角度進行分析和比較.

實現原理
鎖機制的不同
如何得到/釋放鎖
優缺點

1)實現原理

方法一原理:

通過Collections.synchronizedMap()來封裝所有不安全的HashMap的方法,就連toString, hashCode都進行了封裝. 封裝的關鍵點有2處,1)使用了經典的synchronized來進行互斥, 2)使用了代理模式new了一個新的類,這個類同樣實現了Map接口.

private static class SynchronizedMap<K,V>

implements Map<K,V>, Serializable {

// use serialVersionUID from JDK 1.2.2 for interoperability

private static final long serialVersionUID = 1978198479659022715L;

private final Map<K,V> m; // Backing Map

final Object mutex;// Object on which to synchronize

SynchronizedMap(Map<K,V> m) {

if (m==null)

throw new NullPointerException();

this.m = m;

mutex = this;

}

SynchronizedMap(Map<K,V> m, Object mutex) {

this.m = m;

this.mutex = mutex;

}

public int size() {

synchronized(mutex) {return m.size();}

}

//***

//節省空間,刪除了大量類似代碼

//***

public String toString() {

synchronized(mutex) {return m.toString();}

}

private void writeObject(ObjectOutputStream s) throws IOException {

synchronized(mutex) {s.defaultWriteObject();}

}

}

方法二原理:

重新寫了HashMap,比較大的改變有如下幾點.

使用了新的鎖機制(可以理解爲樂觀鎖)稍後詳細介紹

把HashMap進行了拆分,拆分成了多個獨立的塊,這樣在高併發的情況下減少了鎖衝突的可能

public V put(K key, V value) {

if (value == null)

throw new NullPointerException();

int hash = hash(key.hashCode());

return segmentFor(hash).put(key, hash, value, false);

}

2)鎖機制的不同

方法一使用的是的synchronized方法,是一種悲觀鎖.在進入之前需要獲得鎖,確保獨享當前對象,然後做相應的修改/讀取.

方法二使用的是樂觀鎖,只有在需要修改對象時,比較和之前的值是否被人修改了,如果被其他線程修改了,那麼就會返回失敗.鎖的實現,使用的是NonfairSync. 這個特性要確保修改的原子性,互斥性,無法在JDK這個級別得到解決,JDK在此次需要調用JNI方法,而JNI則調用CAS指令來確保原子性與互斥性.讀者可以自行Google JAVA CAS來了解更多. JAVA的樂觀鎖是如何實現的.

當如果多個線程恰好操作到ConcurrentHashMap同一個segment上面,那麼只會有一個線程得到運行,其他的線程會被LockSupport.park(),稍後執行完成後,會自動挑選一個線程來執行LockSupport.unpark().

    V put(K key, int hash, V value, boolean onlyIfAbsent) {

        lock();

        try {

            int c = count;

            if (c++ > threshold) // ensure capacity

                rehash();

            HashEntry<K,V>[] tab = table;

            int index = hash & (tab.length - 1);

            HashEntry<K,V> first = tab[index];

            HashEntry<K,V> e = first;

            while (e != null && (e.hash != hash || !key.equals(e.key)))

                e = e.next;

            V oldValue;

            if (e != null) {

                oldValue = e.value;

                if (!onlyIfAbsent)

                    e.value = value;

            }

            else {

                oldValue = null;

                ++modCount;

                tab[index] = new HashEntry<K,V>(key, hash, first, value);

                count = c; // write-volatile

            }

            return oldValue;

        } finally {

            unlock();

        }

    }

3)如何得到/釋放鎖

得到鎖:

方法一:在Hashmap上面,synchronized鎖住的是對象(不是Class),所以第一個申請的得到鎖,其他線程將進入阻塞,等待喚醒.

方法二:檢查AbstractQueuedSynchronizer.state,如果爲0,則得到鎖,或者申請者已經得到鎖,則也能再辭得到鎖,並且state也加1.

釋放鎖:

都是得到鎖的逆操作,並且使用正確,二種方法都是自動選取一個隊列中的線程得到鎖可以獲得CPU資源.

4)優缺點

方法一:

優點:代碼實現十分簡單,一看就懂.

缺點:從鎖的角度來看,方法一直接使用了鎖住方法,基本上是鎖住了儘可能大的代碼塊.性能會比較差.

方法二:

優點:需要互斥的代碼段比較少,性能會比較好. ConcurrentHashMap把整個Map切分成了多個塊,發生鎖碰撞的機率大大降低,性能會比較好.

缺點:代碼實現稍稍複雜些.

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