Java面試知識點(一)(補充3)如何讓HashMap線程安全

Java HashMap 是非線程安全的。在多線程條件下,容易導致死循環,具體表現爲 CPU 使用率 100%。因此多線程環境下保證 HashMap 的線程安全性,主要有如下幾種方法:

使用 java.util.Hashtable 類,此類是線程安全的。
使用 java.util.concurrent.ConcurrentHashMap,此類是線程安全的。
使用 java.util.Collections.synchronizedMap () 方法包裝 HashMap object,得到線程安全的 Map,並在此 Map 上進行操作。

(一)java.util.Hashtable 類:

下面是 Hashtable 的源碼:

public synchronized V get(Object key) {  
    Entry tab[] = table;  
    …… // 此處省略,具體的實現請參考 jdk 實現  
    }  
  
public synchronized V put(K key, V value) {  
    …… // 具體實現省略,請參考 jdk 實現  
    }  
  
  
public synchronized V remove(Object key) {  
    …… // 具體實現省略,請參考 jdk 實現  
    }  
  
     
    public synchronized void putAll(Map<? extends K, ? extends V> t) {  
        for (Map.Entry<? extends K, ? extends V> e : t.entrySet())  
            put(e.getKey(), e.getValue());  
    }  
  
  
    public synchronized void clear() {  
    …… // 具體實現省略,請參考 jdk 實現  
    }  

上面是 Hashtable 提供的幾個主要方法,包括 get (), put (), remove () 等。注意到每個方法本身都是 synchronized 的,不會出現兩個線程同時對數據進行操作的情況,因此保證了線程安全性,但是也大大的降低了執行效率。因此是不推薦的。

(二)使用 java.util.Collections.synchronizedMap (Map<K,V>) 方法進行封裝。

public static <K,V> Map<K,V> synchronizedMap(Map<K,V> m) {  
    return new SynchronizedMap<K,V>(m);  
    }  
  
  
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 boolean isEmpty(){  
        synchronized(mutex) {return m.isEmpty();}  
        }  
    public boolean containsKey(Object key) {  
        synchronized(mutex) {return m.containsKey(key);}  
        }  

synchronizedMap使用方式

	Map<String,Long> user = Collections.synchronizedMap(new HashMap<>());
    user.put("xsj",1234567l);
    System.out.println(user.get("xsj"));

可以看到 SynchronizedMap 是一個實現了 Map 接口的代理類,該類中對 Map 接口中的方法使用 synchronized 同步關鍵字來保證對 Map 的操作是線程安全的。

既然已經有了 Hashtable, 爲什麼還需要 Collections 提供的這種靜態方法包裝哪?很簡單,這種包裝是 Java Collection Framework 提供的統一接口,除了用於 HashMap 外,還可以用於其他的 Map。當然 除了對 Map 進行封裝,Collections 工具類還提供了對 Collection(比如 Set,List)的線程安全實現封裝方法,具體請參考 java.util.Colletions 實現,其原理和 SynchronizedMap 是一致的。

(三) 使用 java.util.concurrent.ConcurrentHashMap 類。

這是 HashMap 的線程安全版,同 Hashtable 相比,ConcurrentHashMap 不僅保證了訪問的線程安全性,而且在效率上有較大的提高。

相對 HashMap 和 Hashtable, ConcurrentHashMap 增加了 Segment 層,每個 Segment 原理上等同於一個 Hashtable, ConcurrentHashMap 爲 Segment 的數組。下面是 ConcurrentHashMap 的 put 和 get 方法:

final Segment<K,V> segmentFor(int hash) {  
        return segments[(hash >>> segmentShift) & segmentMask];  
    }  
  
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);  
    }  
  
public V get(Object key) {  
        int hash = hash(key.hashCode());  
        return segmentFor(hash).get(key, hash);  
    }

向 ConcurrentHashMap 中插入數據或者讀取數據,首先都要講相應的 Key 映射到對應的 Segment,因此不用鎖定整個類, 只要對單個的 Segment 操作進行上鎖操作就可以了。理論上如果有 n 個 Segment,那麼最多可以同時支持 n 個線程的併發訪問,從而大大提高了併發訪問的效率。另外 rehash () ——rehash就是擴容之後對數據進行重新存儲的過程。操作也是對單個的 Segment 進行的,所以由 Map 中的數據量增加導致的 rehash 的成本也是比較低的。

V put(K key, int hash, V value, boolean onlyIfAbsent) {  
            lock();  
            try {  
                int c = count;  
                if (c++ > threshold) // ensure capacity  
                    rehash();  
  
                 …… // 代碼省略,具體請查看源碼                  
  
            } finally {  
                unlock();  
            }  
        }  
  
V replace(K key, int hash, V newValue) {  
            lock();  
            try {  
                HashEntry<K,V> e = getFirst(hash);  
                
                 …… // 代碼省略,具體請查看源碼     
                 
            } finally {  
                unlock();  
            }  
        }  

可見對 單個的 Segment 進行的數據更新操作都是 加鎖的,從而能夠保證線程的安全性。

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