背景
- HashMap線程不安全:在https://blog.csdn.net/ym15229994318ym/article/details/105436994中提到多線程環境下,使用HashMap進行put操作時存在丟失數據、死鎖的情況,爲了避免這種bug的隱患,官方建議使用ConcurrentHashMap代替HashMap。
效率低下的HashTable容器 - HashTable效率低:HashTable是一個線程安全的類,它使用synchronized來鎖住整張Hash表來實現線程安全,即
每次鎖住整張表讓線程獨佔
,相當於所有線程進行讀寫時都去競爭一把鎖,導致效率非常低下。因爲當一個線程訪問HashTable的同步方法時,其他線程訪問HashTable的同步方法時,可能會進入阻塞或輪詢狀態。如線程1使用put進行添加元素,線程2不但不能使用put方法添加元素,並且也不能使用get方法來獲取元素,所以競爭越激烈效率越低。 - ConcurrentHashMap可以做到讀取數據不加鎖,並且其內部的結構可以讓其在進行寫操作的時候能夠將鎖的粒度保持地儘量地小,允許多個修改操作併發進行,其關鍵在於使用了鎖分離技術。它使用了多個鎖來控制對hash表的不同部分進行的修改。ConcurrentHashMap內部使用段(
Segment)
來表示這些不同的部分,每個段其實就是一個小的HashMap
,它們有自己的鎖。只要多個修改操作發生在不同的段上,它們就可以併發進行。
分段鎖
感謝作者:https://www.cnblogs.com/ITtangtang/p/3948786.html
HashTable容器在競爭激烈的併發環境下表現出效率低下的原因,是因爲所有訪問HashTable的線程都必須競爭同一把鎖,那假如容器裏有多把鎖,每一把鎖用於鎖容器其中一部分數據,那麼當多線程訪問容器裏不同數據段的數據時,線程間就不會存在鎖競爭,從而可以有效的提高併發訪問效率,這就是ConcurrentHashMap所使用的鎖分段技術,首先將數據分成一段一段的存儲,然後給每一段數據配一把鎖,當一個線程佔用鎖訪問其中一個段數據的時候,其他段的數據也能被其他線程訪問。
有些方法需要跨段,比如size()和containsValue(),它們可能需要鎖定整個表而而不僅僅是某個段,這需要按順序鎖定所有段,操作完畢後,又按順序釋放所有段的鎖。這裏“按順序”是很重要的,否則極有可能出現死鎖,在ConcurrentHashMap內部,段數組是final的,並且其成員變量實際上也是final的,但是,僅僅是將數組聲明爲final的並不保證數組成員也是final的,這需要實現上的保證。這可以確保不會出現死鎖,因爲獲得鎖的順序是固定的。
ConcurrentHashMap是由Segment數組結構和HashEntry數組結構組成。Segment是一種可重入鎖ReentrantLock,在ConcurrentHashMap裏扮演鎖的角色,HashEntry則用於存儲鍵值對數據。一個ConcurrentHashMap裏包含一個Segment數組,Segment的結構和HashMap類似,是一種數組和鏈表結構, 一個Segment裏包含一個HashEntry數組,每個HashEntry是一個鏈表結構的元素, 每個Segment守護者一個HashEntry數組裏的元素,當對HashEntry數組的數據進行修改時,必須首先獲得它對應的Segment鎖。
Segment
在ConcurrentHashMap內部,有一個Segment數組,Segment是什麼呢?下圖一目瞭然。
可以理解爲一個自帶鎖的小的HashMap,當往ConcurrentHashMap集合中put一個元素時,先計算應該放在Segment[]數組的那個位置上,再調用對應位置Segment對象的put方法時,會計算應該放在小的HashMap數組的位置,這個小的HashMap中是有一個鎖的,但是他與其他的小HashMap無關,這樣就形成了一種分段鎖。
Segment繼承了ReentrantLock
,所以Segment是一個自帶鎖的類。
Segment類有一個HashEntry<K,V>[] table;
屬性就是我這裏說的小HashMap
ConcurrentHashMap原理
先明白幾個變量
static final int DEFAULT INITIAL_CAPACITY = 16;//初識容量---initialCapacity
static final float DEFAULT_ LOAD_FACTOR = 0.75f;//加載因子---loadFactor
static final int DEFAULT_CONCURRENCY_LEVEL = 16;//併發級別---concurrentLevel
會根據初識容量和併發級別計算出Segment數組的大小
初始化參數
- 在創建ConcurrentMap集合時,提供的構造方法都會調用如下這個構造方法去做初始化,想HashMap一樣依然先會去判斷傳入的參數是否合法,否則拋出異常。
- 還會對傳入的併發級別參數concurrentLevel進行處理,因爲必須要滿足2的冪次方。賦值爲
ssize
。 - 在構造ConcurrentMap集合時,就已經根據處理後的參數,對Segment數組中的小HashMap進行了進行了配置,作爲後面的模板,在插入元素時就不用每次都去計算小HashMap的容量等各個參數了。
這裏可以看出int c = initialCapacity / ssize //ssize是處理後的併發級別
,並且得到的這個c值並不直接作爲小HashMap的容量,也是經過位運算處理,使其變爲2的冪次方。最終賦值給cap
變量。 - 接着直接創建容量爲
cap
的HashEnry[]
,並賦值給創建的Segment對象。 - 處理後的
ssize
作爲Segment[]
的容量。
這裏的ssize和cap都做了位運算處理,使其變爲2的指數次冪。原因:(在和元素的hash值做位運算時可以很好的散列,既要達到最可能的平均分配hashMap的value的在table數組的各個index,又要用二進制計算實現存取效率,就要要求hashMap的容量必須爲2的冪次方;)
添加元素
添加元素時,邏輯是先找Segment數組下標位置,再在該Segment對象中的table數組找小HashMap下標位置。所以最後會調用segment對象的put方法,return s.put(key,hash,value,false);
判斷Segment對應的這個下標下是否有元素(segment對象),爲null就構造一個。
會去判斷Segment對應的這個下標下是否有Segment對象,沒有就構造一 個,在ensureSegment()
方法中進行構造。這時構造就不需要去重新計算這個Segment對象的小HashMap的參數了,因爲在創建ConcurrentMap對象時就已經初始化好了,放在Segment數組的第一個位置作爲模板,使用該模板的參數即可。
這時Segment類的put
方法。
這樣就形成了分段鎖,解決了HashMap線程不安全問題,也相對解決了HashTable效率慢的問題。
jdk7之前ConcurrentHashMap主要採用鎖機制,在對某個Segment進行操作時,將該Segment鎖定,不允許對其進行非查詢操作,而在jdk8之後採用CAS無鎖算法,這種樂觀操作在完成前進行判斷,如果符合預期結果纔給予執行,對併發操作提供良好的優化.