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切分成了多個塊,發生鎖碰撞的幾率大大降低,性能會比較好.
缺點:代碼實現稍稍復雜些.
2種辦法讓HashMap線程安全