1. 程式人生 > 實用技巧 >Java集合之ConcurrentHashMap解析

Java集合之ConcurrentHashMap解析

  上一篇介紹了HashMap的資料結構:陣列+單鏈表(jdk 1.8,當連結串列長度達到8後,連結串列將會被轉換為紅黑樹結構)。日常開發中我們經常使用,隨著業務規模、場景的不斷複雜發展,多執行緒開發越來越多的進入到我們日常開發中,那麼問題就來了,HashMap是執行緒安全的嗎?答案是否定的,保證HashMap的執行緒安全需要我們開發中自行維護。那麼有沒有執行緒安全的集合框架呢?答案是肯定的,java.util包下的HashTab類,就是一種執行緒安全的Map容器。

 HashTabe

  為了更快速的理解HashTabe,接下來就結合HashMap做下對比,幫助我們更直觀的認識。

  1、HashTabe預設的容量是11,而HashMap是16

  2、HashTabe陣列表是一旦建立就構造,屬於餓漢模式,而HashMap是在第一次put時的resize構造

  3、HashTabe資料結構是陣列+單向連結串列,而HashMap則是陣列+單向連結串列+紅黑樹

  4、HashTabe中連結串列Node節點採用頭插法,而HashMap則是採用尾插法

  5、HashTabe通過對put、get、remove、size等方法新增synchronized關鍵字保證執行緒安全,而HashMap本身並沒有保證執行緒安全的相關處理,需要業務使用時自行保障

  6、HashTabe的鍵值均不能為null,而HashMap支援鍵值為null

  下面我們看一下Hashtabe的原始碼,驗證一下我們上面提到的內容,首先我們看一下構造方法:  

public Hashtable(int initialCapacity, float loadFactor) {
    if (initialCapacity < 0)
        throw new IllegalArgumentException("Illegal Capacity: "+
                                           initialCapacity);
    if (loadFactor <= 0 || Float.isNaN(loadFactor))
        throw new IllegalArgumentException("Illegal Load: "+loadFactor);

    
if (initialCapacity==0) initialCapacity = 1; this.loadFactor = loadFactor;
   // 對陣列進行初始化 table
= new Entry<?,?>[initialCapacity]; // 擴容閾值 = 陣列容量 * 負載係數;最大值為:0x7fffffff - 8 + 1 threshold = (int)Math.min(initialCapacity * loadFactor, MAX_ARRAY_SIZE + 1); } public Hashtable(int initialCapacity) { this(initialCapacity, 0.75f); } public Hashtable() { this(11, 0.75f); } public Hashtable(Map<? extends K, ? extends V> t) { this(Math.max(2*t.size(), 11), 0.75f); putAll(t); }

  通過原始碼我們可以看到,無參構造方法中,系統預設為我們定義了陣列的容量和負載係數,並且在呼叫構造方法時,系統會預設為我們建立初始陣列,這裡和HashMap有所不同,大家可以做下對比,便於更好的記憶。

  下面我們以put方法為例,分析一下上面我們提到的幾個點

public synchronized V put(K key, V value) {
    // 檢查值value是否為空
    if (value == null) {
        throw new NullPointerException();
    }

    // 檢查鍵是否存在於hash表中
    Entry<?,?> tab[] = table;
    // 鍵不能為空,否則會導致空指標
    int hash = key.hashCode();
    // 這裡獲取key陣列下標有別於HashMap
    int index = (hash & 0x7FFFFFFF) % tab.length;
    @SuppressWarnings("unchecked")
    Entry<K,V> entry = (Entry<K,V>)tab[index];

    // 遍歷陣列當前節點的單鏈表查詢鍵是否已存在
    for(; entry != null ; entry = entry.next) {
        if ((entry.hash == hash) && entry.key.equals(key)) {
            V old = entry.value;
            // 鍵值存在直接更新,並返回原鍵值
            entry.value = value;
            return old;
        }
    }

    // 當鍵不存在時,將鍵值插入指定連結串列中
    addEntry(hash, key, value, index);
    return null;
}

private void addEntry(int hash, K key, V value, int index) {
    modCount++;

    // 檢查陣列長度是否達到閾值,達到閾值對陣列進行擴容
    Entry<?,?> tab[] = table;
    if (count >= threshold) {
        // Rehash the table if the threshold is exceeded
        rehash();

        tab = table;
        hash = key.hashCode();
        // 陣列擴容後,以新陣列長度計算鍵的陣列下標
        index = (hash & 0x7FFFFFFF) % tab.length;
    }

    // 以鍵值建立新的Node節點,將陣列該位置的原頭節點,設定為新節點的next
    @SuppressWarnings("unchecked")
    Entry<K,V> e = (Entry<K,V>) tab[index];
    tab[index] = new Entry<>(hash, key, value, e);
    count++;
}

protected void rehash() {
    int oldCapacity = table.length;
    Entry<?,?>[] oldMap = table;

    // 建立一個數組容量擴大2倍 + 1的新陣列
    int newCapacity = (oldCapacity << 1) + 1;
    if (newCapacity - MAX_ARRAY_SIZE > 0) {
        if (oldCapacity == MAX_ARRAY_SIZE)
            // Keep running with MAX_ARRAY_SIZE buckets
            return;
        newCapacity = MAX_ARRAY_SIZE;
    }
    Entry<?,?>[] newMap = new Entry<?,?>[newCapacity];

    modCount++;
    // 更新新陣列的閾值
    threshold = (int)Math.min(newCapacity * loadFactor, MAX_ARRAY_SIZE + 1);
    table = newMap;

    // 迴圈遍歷進行陣列資料遷移
    for (int i = oldCapacity ; i-- > 0 ;) {
        for (Entry<K,V> old = (Entry<K,V>)oldMap[i] ; old != null ; ) {
            Entry<K,V> e = old;
            old = old.next;

            int index = (e.hash & 0x7FFFFFFF) % newCapacity;
            e.next = (Entry<K,V>)newMap[index];
            newMap[index] = e;
        }
    }
}

  到這裡大家應該對於Hashtable已經有了一個清晰的認識了,這裡提到synchronized關鍵字,我們知道synchronized有兩個維度:1、類維度加鎖;2、物件維度加鎖,Hashtabe採用的是什麼維度呢?答案是物件維度加鎖。這樣做有產生什麼樣的問題呢?這要簡單聊一下多執行緒的使用場景,我們為什麼要用多執行緒?我們知道單執行緒下我們的任務是序列執行的,對於多CPU系統中,無法發揮多核心的優勢,使用多執行緒將一個任務拆分為並行的多個任務,在多CPU系統並行執行任務,從而提高任務的執行效率。那麼問題就來了,Hashtabe通過物件維度加鎖,當存在多個執行緒並行操作時,就會存在鎖競爭,這也是為什麼說HashTable慢的原因所在。synchronized關鍵字加鎖是對整個物件進行加鎖,也就是說在進行put等修改Hash表的操作時,鎖住了整個Hash表,從而使得其表現的效率低下。