HashTable
HashTable是線程安全的,那麼HashTable做了什麼操作才實現了HashMap沒做到的線程安全呢???
寫個簡單的demo:
進入synchronizedMap內部看一下:
聲明瞭一個mutex修飾對象的成員,使用互斥鎖包圍起來保證了內容互斥,串行化訪問以此來保證線程安全。
HashTable也是一樣的和HashMap實現邏輯沒有什麼區別,是用同樣的去加鎖而已:
但是串行化訪問效率就會降低,那麼既要保證線程安全又要保證效率要怎麼辦呢?這個時候ConcurrentHashMap就出現了。
ConcurrentHashMap
如果不用ConcurrentHashMap,那麼要如何去優化HashTable呢???
通過鎖粒度細化,整鎖拆解成多個鎖進行優化
在JDK5中的ConcurrentHashMap確實這麼實現的,通過分段鎖的Segment實現的結構是數組加上鍊表。而Segment可以對應頭結點去存放默認的16把鎖,區別是比起HashMap給每個頭結點配一把鎖,從串行化的HashTable簡單對比效率提升了16倍。
如果7號鎖下的線程去訪問數組的話,其他節點佔有的線程也可以去訪問這些資源不會被阻塞,只有訪問相同的Segment纔會被阻塞。
但是每個bucket都用不同的鎖去管理,性能上來說還是存在一定的缺陷,JDK8中ConcurrentHashMap使用CAS+synchronized使鎖變得更加細粒度化,同時又進行了優化。
下圖是ConcurrentHashMap的圖解:
JDK8中ConcurrentHashMap隨着最新的HashMap,使用了數組+鏈表+紅黑樹這種結構在效率上進行一定的提升。
下面來看一下ConcurrentHashMap的源碼:
從成員變量來看,和HashMap存在相似之處:
最重要的是sizeCtl的成員變量,是JUC下面ConcurrentHashMap十分關鍵的一個成員變量。
是一個大小控制的標識符,哈希表初始化或者擴容的和時候的一個控制位標示量,負數代表正在進行初始化或者擴容操作,-1代表正在進行初始化,其他的負數代表有n-1個線程正在等待擴容,整合和爲0代表還沒有被初始化。
被volatile修飾,是多線程可見的:
ConcurrentHashMap是使用CAS+synchronized做到高效保證線程安全的,來看看put方法:
簡單描述一下:
- 使用for循環,存在CAS操作要保證不斷的去請求操作,之後判斷是否爲空,是空的話就去初始化;
- 如果不是空,進行哈希尋址找到頭結點的存儲位置;
- 如果這個位置不存在,使用CAS操作創建出來,添加失敗break進入下一個循環;
- 如果發現這個key已經存在,說明ConcurrentHashMap正在進行remove,remove說明正在進行擴容就協助進行擴容,執行helpTransfer操作。
最後一種情況是哈希碰撞:
這個時候會鎖住鏈表或者紅黑二叉樹的頭結點,就是數組元素,操作如下:
此外ConcurrentHashMap還可以構建本地緩存來降低程序的計算量和負責度。。。
總結一下ConcurrentHashMap的put方法的實現邏輯:
- 判斷Node[]數組是否進行了初始化,沒有進行初始化操作;
- 通過hash定位數組的索引座標是否有Node節點,如果沒有使用CAS進行添加,添加失敗進行下一次循環;
- 檢查到內部正在擴容,就幫助一起擴容;
- 如果鏈表頭結點是空,用synchronized鎖住頭結點;
(1)、如果是Node進行鏈表的添加操作;
(2)、如果是TreeNode進行樹添加操作;
(3)、如果是ReservationNode則拋出異常,ReservationNode和ConcurrentHashMap本地緩存相關; - 判斷鏈表長度達到8將鏈表轉換成紅黑樹結構。
總結一下ConcurrentHashMap在JDK8和JDK5版本的差異:
總體還是使用的鎖分離的思想就是比Segment更加細緻,首先使用無鎖操作CAS插入頭結點,失敗就循環重試,如果頭結點已經存在就嘗試獲取頭結點的同步鎖再進行操作。
總結一下HashMap、HashTable、ConcurrentHashMap三者的區別:
- HashMap線程不安全,數據結構是數組+鏈表+紅黑樹;
- HashTable線程安全,數據結構是數組+鏈表;
- ConcurrentHashMap線程安全,使用CAS+同步鎖,數據結構是數組+鏈表+紅黑樹;
- HashMap的key和value可以爲null,其他兩個不可以。