多線程與高併發08-併發容器(一)

Java下的併發容器

預備知識-HASH

就是把任意長度的輸入(又叫作預映射, pre-image),經過散列算法,變換成固定長度的輸出,該輸出就是散列值。這種轉換是一種壓縮映射,也就是,散列值的空間一般遠小於輸入的空間,不一樣的輸入可能會散列成相同的輸出,因此不可能從散列值來肯定惟一的輸入值。簡單的說就是一種將任意長度的消息壓縮到某一固定長度的消息摘要的函數。經常使用HASH函數:直接取餘法、乘法取整法、平方取中法java

處理衝突方法
  • 開放尋址法
  • 再散列法
  • 鏈地址法(拉鍊法,經常使用)
經常使用hash算法的介紹:(1) MD4(2) MD5它對輸入仍以512位分組,其輸出是4個32位字的級聯(3) SHA-1

預備知識-位運算

  • 有符號右移>>(若正數,高位補0,負數,高位補1)
  • 有符號左移<<
  • 無符號右移>>>(不論正負,高位均補0)
  • 取模a % (2^n) 等價於 a & (2^n - 1),因此在map裏的數組個數必定是2的乘方數,計算key值在哪一個元素中的時候,就用位運算來快速定位
  • a * 2 等價於 a<<1
  • a / 2 等價於 a>>1

經常使用集合容器

  • Hashtable:使用synchronized實現同步,是鎖了整個容器,已經淘汰
  • ArrayList linkedlist HashSet HashMap:未考慮多線程安全(未實現同步)
  • Collections.synchronizedXXX:同步工具類,能夠把非同步容器轉爲同步容器,仍然使用synchronized關鍵字實現同步,鎖整個容器
  • ConcurrentHashMap:用CAS+sync+分段鎖實現了鎖,在多線程讀取的時候效率很是高
  • TreeMap(TreeSet):紅黑樹排序Map,線程不安全
  • LinkedHashMap:用一個列表維護插入Map的節點時的插入順序,天生實現了LRU算法
  • ConcurrentSkipListMap:高併發而且排序,跳錶實現CAS操做比Tree簡單,因此沒有ConcurrentTreeMap
  • CopyOnWriteArrayList(CopyOnWriteArraySet):讀的時候不加鎖,寫的時候加鎖,而且把原數組複製一份,長度+1,把新元素加入,在把引用指向新數組,讀特別多,寫特別少用CopyOnWriteArrayList

JDK1.7中併發條件下HashMap的死鎖問題

在多線程環境下,使用HashMap進行put操做會引起多線程擴容,因爲JDK1.7中HashMap的鏈表採用頭插法,多線程擴容後會造成環形數據結構,一旦造成環形數據結構,Entry的next節點永遠不爲空,就會產生死循環獲取Entry,致使在get遍歷查找元素的時候進入死循環,使得CPU利用率接近100%
image.png面試

HashMap之因此在併發下的擴容形成死循環,是由於,多個線程併發進行時,由於一個線程先期完成了擴容,將原Map的鏈表從新散列到本身的表中,而且鏈表變成了倒序,後一個線程再擴容時,又進行本身的散列,再次將倒序鏈表變爲正序鏈表。因而造成了一個環形鏈表,當get表中不存在的元素時,形成死循環

ConcurrentHashMap

JDK1.7下的實現

image.png

  • JDK1.7下采用了分段鎖的概念,分段(Segment)的個數在初始化時就已經指定,後續不可擴展,這裏的鎖是CAS可重入鎖
  • 每一個Segment下掛載一個table,用來存儲HashEntry,衝突採用鏈表方式解決
  • 每一個table容量超過擴容因子,則會擴容,申請新空間,從新計算Hash位置,並移動元素

JDK1.8下的實現

image.png

  • JDK1.8下面取消了1.7的Segment結構,直接使用數組
  • 鎖的粒度更細,是table裏面的每一個頭節點
  • Hash衝突的解決引入了紅黑樹,而且設置了閥值,鏈表長度超過8,鏈表轉紅黑樹,長度低於6,紅黑樹轉鏈表
  • 鎖採用了CAS分段鎖+synchronized關鍵字(JDK1.8對sync作了不少優化)
  • 擴容時採用了多工做線程協做模式,每一個線程負責固定步長原子的遷移

併發下的Map常見面試題彙總

HashMap 和 HashTable 有什麼區別?

①、HashMap 是線程不安全的,HashTable 是線程安全的算法

②、因爲線程安全,因此 HashTable 的效率比不上 HashMap數組

③、HashMap最多隻容許一條記錄的鍵爲null,容許多條記錄的值爲null,而 HashTable 不容許安全

④、HashMap 默認初始化數組的大小爲16,HashTable 爲 11,前者擴容時,擴大兩倍,後者擴大兩倍+1數據結構

⑤、HashMap 須要從新計算 hash 值,而 HashTable 直接使用對象的 hashCode多線程

Java 中的另外一個線程安全的與 HashMap 極其相似的類是什麼?一樣是線程安全,它與 HashTable 在線程同步上有什麼不一樣?

ConcurrentHashMap 類(是 Java併發包 java.util.concurrent 中提供的一個線程安全且高效的 HashMap 實現)。併發

HashTable 是使用 synchronize 關鍵字加鎖的原理(就是對對象加鎖);函數

而針對 ConcurrentHashMap,在 JDK 1.7 中採用分段鎖的方式;JDK 1.8 中直接採用了CAS(無鎖算法)+ synchronized,也採用分段鎖的方式並大大縮小了鎖的粒度。高併發

HashMap & ConcurrentHashMap 的區別?

除了加鎖,原理上無太大區別

另外,HashMap 的鍵值對容許有nullkey爲null放在table的0號位置),可是ConCurrentHashMap 都不容許

在數據結構上,紅黑樹相關的節點類

爲何 ConcurrentHashMap  比 HashTable 效率要高?

HashTable 使用一把鎖(鎖住整個鏈表結構)處理併發問題,多個線程競爭一把鎖,容易阻塞

ConcurrentHashMap  

JDK 1.7 中使用分段鎖(ReentrantLock + Segment + HashEntry),至關於把一個 HashMap 分紅多個段,每段分配一把鎖,這樣支持多線程訪問。鎖粒度:基於 Segment,包含多個 HashEntry。

JDK 1.8 中使用 CAS + synchronized + Node + 紅黑樹。鎖粒度:Node(首結點)(實現 Map.Entry<K,V>)。鎖粒度下降了。

針對 ConcurrentHashMap 鎖機制具體分析(JDK 1.7 VS JDK 1.8)?

JDK 1.7 中,採用分段鎖的機制,實現併發的更新操做,底層採用數組+鏈表的存儲結構,包括兩個核心靜態內部類 SegmentHashEntry

①、Segment 繼承 ReentrantLock(重入鎖) 用來充當鎖的角色,每一個 Segment 對象守護每一個散列映射表的若干個桶

②、HashEntry 用來封裝映射表的鍵-值對

③、每一個桶是由若干個 HashEntry 對象連接起來的鏈表

JDK 1.8 中,採用Node + CAS + Synchronized來保證併發安全。取消類 Segment,直接用 table 數組存儲鍵值對;當 HashEntry 對象組成的鏈表長度超過 TREEIFY_THRESHOLD 時,鏈表轉換爲紅黑樹,提高性能。底層變動爲數組 + 鏈表 + 紅黑樹

ConcurrentHashMap 在 JDK 1.8 中,爲何要使用內置鎖 synchronized 來代替重入鎖 ReentrantLock?
  • JVM 開發團隊在1.8中對 synchronized作了大量性能上的優化,並且基於 JVM 的 synchronized 優化空間更大,更加天然。
  • 在大量的數據操做下,對於 JVM 的內存壓力,基於 API  的 ReentrantLock 會開銷更多的內存。
ConcurrentHashMap 簡單介紹?

①、重要的常量:

private transient volatile int sizeCtl;

當爲負數時,-1 表示正在初始化,-N 表示 N - 1 個線程正在進行擴容

當爲 0 時,表示 table 尚未初始化

當爲其餘正數時,表示初始化或者下一次進行擴容的大小

②、數據結構:

Node 是存儲結構的基本單元,繼承 HashMap 中的 Entry,用於存儲數據

TreeNode 繼承 Node,可是數據結構換成了二叉樹結構,是紅黑樹的存儲結構,用於紅黑樹中存儲數據

TreeBin 是封裝 TreeNode 的容器,提供轉換紅黑樹的一些條件和鎖的控制

③、存儲對象時(put() 方法):

  1. 若是沒有初始化,就調用 initTable() 方法來進行初始化
  2. 若是沒有 hash 衝突就直接 CAS 無鎖插入
  3. 若是須要擴容,就先進行擴容
  4. 若是存在 hash 衝突,就加鎖來保證線程安全,兩種狀況:一種是鏈表形式就直接遍歷到尾端插入,一種是紅黑樹就按照紅黑樹結構插入
  5. 若是該鏈表的數量大於閥值 8,就要先轉換成紅黑樹的結構,break 再一次進入循環
  6. 若是添加成功就調用 addCount() 方法統計 size,而且檢查是否須要擴容

④、擴容方法 transfer():默認容量爲 16,擴容時,容量變爲原來的兩倍

helpTransfer():調用多個工做線程一塊兒幫助進行擴容,這樣的效率就會更高

⑤、獲取對象時(get()方法):

  1. 計算 hash 值,定位到該 table 索引位置,若是是首結點符合就返回;
  2. 若是遇到擴容時,會調用標記正在擴容結點 ForwardingNode.find()方法,查找該結點,匹配就返回;
  3. 以上都不符合的話,就往下遍歷結點,匹配就返回,不然最後就返回 null
ConcurrentHashMap 的併發度是什麼?

1.7中程序運行時可以同時更新 ConccurentHashMap 且不產生鎖競爭的最大線程數。默認爲 16,且能夠在構造函數中設置。當用戶設置併發度時,ConcurrentHashMap 會使用大於等於該值的最小2冪指數做爲實際併發度(假如用戶設置併發度爲17,實際併發度則爲32)

1.8中併發度則無太大的實際意義了,主要用處就是當設置的初始容量小於併發度,將初始容量提高至併發度大小

相關文章
相關標籤/搜索