1. 程式人生 > >簡單學習ConcurrentHashMap實現執行緒安全的原理

簡單學習ConcurrentHashMap實現執行緒安全的原理

  提到多執行緒肯定想到資料的執行緒安全問題如何解決,util包中的Hashtable,Vector都是執行緒安全的,最初的時候也都會選擇這幾種資料儲存方式,在前幾年面試的時候也經常會被問到Hashtable與HashMap,Vector和ArrayList的區別。簡單看一下Hashtable,Vector執行緒安全的實現方式,這兩種都是直接對方法加synchronized,直接上程式碼,看一下Hashtable。

public synchronized V get(Object key) { //方法內部程式碼不再貼了,與這次學的東西無關 } 
public synchronized V put
(K key, V value) {}

Vector的方式也是這樣。個別沒有加上synchronized關鍵字的public方法,也是呼叫了物件內部的同步方法實現了同步或者用Collections.synchronized方式加上同步,使該方法實現同步。除了這樣實現執行緒安全,還可以直接使用Collections.SynchronizedCollection對其他非執行緒安全的資料結構加鎖實現執行緒安全。
  不管是對方法加synchronized,還是使用Collections.SynchronizedCollection實現同步,對物件的修改或者查詢時,鎖住的都是整個物件。 上面的實現方式確實解決了大部分執行緒安全問題,但隨之而來的確實效能的下降。舉個例子:用一個Hashtable儲存了使用者資訊,每當一個使用者有修改甚至查詢請求時,整個Hashtable就要被鎖住,也就是說,在這個玩家資訊資料處理期間,所有使用者的相關請求執行緒全部要等待,使用者量級小的時候也許感覺不出來卡頓,如果放在當前網際網路環境下,那將是災難性的。
  進入正題,Java1.5之後加入了java.util.concurrent 包,這個包包含有一系列能夠讓 Java 的併發程式設計變得更加簡單輕鬆的類。在這個包被新增以前,你需要自己去動手實現能夠解決以上問題的工具包。這次主要學習一下ConcurrentHashMap的實現原理。
  ConcurrentHashMap定義了Segment內部類,看一下程式碼:

//Segment繼承了ReentrantLock重入鎖(這個概念這次先不看)
static final class Segment<K,V> extends ReentrantLock implements Serializable {
    //HashEntry與HashMap中類似,可以理解為一個單向連結串列元素,作為存放相同hash值不同key的鍵值對
    //這樣一個Segment就相當於一個HashMap
    transient volatile HashEntry<K,V>[] table;
    V put(K key, int hash, V value, boolean
onlyIfAbsent) { //在對Segment進行操作時,對當前物件加鎖 lock(); try { //資料操作 } finally { unlock(); } } }

ConcurrentHashMap通過陣列形式存放多個Segment,用key的hash值做一次再hash當做下標識別當前鍵值對存放在哪個segment裡。

  final Segment<K,V>[] segments;
  public V put(K key, V value) {
        if (value == null)
            throw new NullPointerException();
        //用key的hashCode再做一次hash
        int hash = hash(key.hashCode());
        return segmentFor(hash).put(key, hash, value, false);
    }

在對segment元素進行操作時加鎖,這樣當其它人執行緒操作當前ConcurrentHashMap物件時,只要key的hash值與加鎖的值不同,就可以直接操作其它Segment元素。
示意圖:
這裡寫圖片描述

以上原理皆為JDK1.6版本原始碼,JDK1.8對ConcurrentHashMap做了大量修改,不僅去除了Segment概念,直接採用HashEntry儲存資料,將鎖也直接載入HashEntry,大大減少了資料衝突引起的延遲,還引入了紅黑樹演算法,使hash之後在陣列的分佈更加均勻。對JDK1.8的原始碼學習之後補上。