1. 程式人生 > >Java併發容器(一) CocurrentHashMap的應用及實現

Java併發容器(一) CocurrentHashMap的應用及實現

CocurrentHashMap的優勢

首先常用三種HashMap包括HashMap,HashTable和CocurrentHashMap:

  • HashMap在併發程式設計過程中使用可能導致死迴圈,因為插入過程不是原子操作,每個HashEntry是一個連結串列節點,很可能在插入的過程中,已經設定了後節點,實際還未插入,最終反而插入在後節點之後,造成鏈中出現環,破壞了連結串列的性質,失去了尾節點,出現死迴圈。
  • HashTable因為內部是採用synchronized來保證執行緒安全的,但線上程競爭激烈的情況下HashTable的效率下降得很快因為synchronized關鍵字會造成程式碼塊或方法成為為臨界區(對同一個物件加互斥鎖),當一個執行緒訪問臨界區的程式碼時,其他執行緒也訪問同一臨界區時,會進入阻塞或輪詢狀態。究其原因,實際上是有獲取鎖意向的執行緒的數目增加,但是鎖還是隻有單個,導致大量的執行緒處於輪詢或阻塞,導致同一時間段有效執行的執行緒的增量遠不及執行緒總體增量。
    • 在查詢時,尤其能夠體現出CocurrentHashMap在效率上的優勢,HashTable使用Sychronized關鍵字,會導致同時只能有一個查詢在執行,而Cocurrent則不採取加鎖的方法,而是採用volatile關鍵字,雖然也會犧牲效率,但是由於Sychronized,於該文末尾繼續討論。
  • CocurrentHashMap利用鎖分段技術增加了鎖的數目,從而使爭奪同一把鎖的執行緒的數目得到控制。
    • 鎖分段技術就是對資料集進行分段,每段競爭一把鎖,不同資料段的資料不存在鎖競爭,從而有效提高 高併發訪問效率
    • CocurrentHashMap在get方法是無需加鎖的,因為用到的共享變數都採用volatile關鍵字修飾,巴證共享變數線上程之間的可見性(每次讀取都先同步快取和記憶體,直接從記憶體中獲取值,雖然不是原子操作,但根據JAVA記憶體模型的happen before原則,對volatile欄位的寫入操作先於讀操作,能夠保證不會髒讀),volatile為了讓變數提供執行緒之間的記憶體可見性,會禁止程式執行結果的重排序(導致快取優化的效果降低)

CocurrentHashMap的結構

  • CocurrentHashMap是由Segment陣列HashEntry陣列組成。
  • Segment是重入鎖(ReentrantLock),作為一個數據段競爭鎖,每個HashEntry一個連結串列結構的元素,利用Hash演算法得到索引確定歸屬的資料段,也就是對應到在修改時需要競爭獲取的鎖。

segments陣列的初始化

  • 首先簡單描述一下原始碼中變數的含義:
變數名稱 描述
cocurrencyLevel 能夠滿足的併發訪問數量,即最多同時可以有多少執行緒同時訪問一個CocurrencyHashMap物件(個人的理解)
ssize segments陣列的長度(因為要利用位運算和hash演算法獲取索引,故必須是2n),而且在確定長度時能夠保證複雜度在O(logn2)
segmentShift 雜湊後的32中的高位表示segments的索引,代表作無符號右移的偏移量
segmentMask 對應與segment的ssize-1,有效的二進位制位都為1,可以通過與雜湊後的數值與運算得到segment的索引
threshold 一個segment的容量
initialCapacity CocurrentHashMap的初始化容量
cap 一個segment中HashEntry陣列的長度
loadFactor 負載因子,我理解的是負載因子越大會導致出現衝突的概率增大,設定的過小又會浪費空間,所以應該根據實際情況考慮空間和時間上的平衡
  • 首先計算出segment陣列的長度ssize,並且計算出與ssize關聯的segmentShift和segmentMask
if ( cocurrencyLevel > MAX_SEGMENTS )
    cocurrencyLevel = MAX_SEGMENTS;
//如果cocurrencyLevel大於上限,那麼取值為上限,
//上限定義為65535,決定了重入鎖的segments的數目
int sshift = 0;
int ssize = 1;
//找到大於等於concurrencyLevel的最小的2^n作為segments的大小
while ( ssize < concurrencyLevel ){
    ++sshift;//記錄偏移量,為了以後通過與運算獲取segment的索引
    ssize <<=1;
}
segmentShift = 32 - sshift;//說明只有高sshift位作為segment的索引
segmentMask = ssize - 1;//能直接通過與運算獲取segment的索引
this.segments = Segment.newArray(ssize);
//靜態工廠方法構造ssize大小的segment陣列
  • 初始化每個segment,因為已經知道segment陣列的規模,將ConcurrentHashMap的邏輯上持有的HashEntry均分到每個Segment上,因為是雜湊,所以要loadFactor來定義負載率,來保證segment適時的拓充,來避免散列表和資料規模相近時,衝突加重的風險。
if ( initialCapacity > MAXIMUM_CAPACITY )
    initialCapacity = MAXIMUM_CAPACITY;
int c = initCapacity / ssize;
//整個cocurrentHashMap的容量由所有的segment均攤
if ( c * ssize < initCapacity )
    ++c;
int cap = 1;//segment中的hashEntry陣列的長度
while ( cap < c )
    cap <<= 1;
//設定loadFactor,保證雜湊的高效性的同時也保證空間浪費相對有限
for ( int i = 0 ; i < this.segments.length ; i++ )
    this.segments[i] =  new Segments<K,V>(cap , loadFactor);
//最終計算出segment的容量threshold=(int)cap*loadFactor;
  • 定位segment,定位segment尤其重要,如果太多元素集中在少數幾個segment中會導致CocurrentHashMap的效率得不到優化,因為同一個segment中的修改還是要競爭鎖,所以選擇合適的hash演算法儘可能地將元素分到不同的segment中是目標。
    • CocurrentHashMap採用的是對元素的HashCode進行再Hash來減少衝突
    • CocurrentHashMap採用的是根據Wang/JenkinsHash改進的hash演算法,該演算法具有雪崩性(引數一位數字變化,結果都有半數左右(二進位制位)發生變化)
final Segment<K,V> segmentFor ( int hash ){
    //首先根據segmentShift無符號右移,得到表示segment所以的高位,
    //然後與掩碼邏輯與得到segment的索引,定位到segment
    return segments[(hash>>>segmentShift)&segmentMask];
}

CocurrentHashMap的操作

  • Segment的get操作是不需要加鎖的。因為volatile修飾的變數保證了執行緒之間的可見性
  • Segment的put操作是需要加鎖的,在插入時會先判斷Segment裡的HashEntry陣列是否會超過容量(threshold),如果超過需要對陣列擴容,翻一倍。然後在新的陣列中重新hash,為了高效,CocurrentHashMap只會對需要擴容的單個Segment進行擴容
  • CocurrentHashMap獲取size的時候要統計Segments中的HashEntry的和,如果不對他們都加鎖的話,無法避免資料的修改造成的錯誤,但是如果都加鎖的話,效率又很低。所以CoccurentHashMap在實現的時候,巧妙地利用了在累加過程中發生變化的機率很小的客觀條件,在獲取count時,不加鎖的計算兩次,如果兩次不相同,在採用加鎖的計算方法。採用了一個高效率的剪枝防止很大概率地減少了不必要額加鎖。

一點理解

synchronized(其實感覺是可以被重入鎖和Condition完全取代的)和volatile的取捨:

  • 首先的區別就在於是否是原子操作,也就是單一的不可分割的操作,在多執行緒中,原子操作能夠保證不受到其他執行緒的影響
  • synchonized就實現了原子性操作,不同的執行緒互斥地進入臨界程式碼區,而且是記憶體可見的,也就是每個執行緒進入臨界區時,都是從記憶體中獲取的值,不會因為快取而出現髒讀。
  • volatile實現了記憶體可見性,會將修改的值直接寫入內容,並且登出掉之前對於該變數的快取,而且禁止了指令的排序。但是它不是原子操作!!

參考書目

  • Java多執行緒程式設計實戰指南
  • Java併發程式設計的藝術
  • Java多執行緒程式設計核心技術

相關推薦

Java併發容器() CocurrentHashMap應用實現

CocurrentHashMap的優勢 首先常用三種HashMap包括HashMap,HashTable和CocurrentHashMap: HashMap在併發程式設計過程中使用可能導致死迴圈,因為插入過程不是原子操作,每個HashEntry是一個連結串

JAVA併發程式設計】--synchronized應用解析

        相信大多數同學在開始接觸併發程式設計的時候,首先了解的就是synchronized關鍵字的修飾,被synchronized修飾的方法或程式碼塊都可以解決多執行緒安全問題。在Java SE1.6版本之前,我們稱之為重量級鎖。因為它在獲取共享鎖的時候是對CPU的獨

Java併發(四):volatile的實現原理 Java併發):Java記憶體模型乾貨總結

synchronized是一個重量級的鎖,volatile通常被比喻成輕量級的synchronized volatile是一個變數修飾符,只能用來修飾變數。 volatile寫:當寫一個volatile變數時,JMM會把該執行緒對應的本地記憶體中的共享變數重新整理到主記憶體。 volatile讀:當讀一

Java併發容器ConcurrentHashMap原理HashMap死迴圈原因的分析

HashMap是我們最常用的資料結構之一,它方便高效,但遺憾的是,HashMap是執行緒不安全的,在併發環境下,在HashMap的擴容過程中,可能造成散列表的迴圈鎖死。而執行緒安全的HashTable使用了大量Synchronized鎖,導致了效率非常低下。幸運的是,併發程

Java併發程式設計之CountDownLatch,CyclicBarrier實現組執行緒相互等待、喚醒

java多執行緒應用場景不少,有時自己編寫程式碼又不太容易實現,好在concurrent包提供了不少實現類,還有google的guava包更是提供了一些最佳實踐,這讓我們在面對一些多執行緒的場景時,有了不少的選擇。這裡主要是看幾個涉及到多執行緒等待的工具類。一 CountDo

CocurrentHashMap應用實現

首先常用三種HashMap包括HashMap,HashTable和CocurrentHashMap: HashMap在併發程式設計過程中使用可能導致死迴圈,因為插入過程不是原子操作,每個HashEntry是一個連結串列節點,很可能在插入的過程中,已經設定了後節點,實際還未

d指針在Qt上的應用實現(有圖,很清楚)

rhel -name spa 自動 版本庫 留空 擴展 vat 因此 Qt為了使其動態庫最大程度上實現二進制兼容,引入了d指針的概念。那麽為什麽d指針能實現二進制兼容呢?為了回答這個問題,首先弄清楚什麽是二進制兼容?所謂二進制兼容動態庫,指的是一個在老版本庫下運行的程序,在

【轉】Java學習---快速掌握RPC原理實現

消費者 阿裏 局限 kryo nes 很多 cal 網絡 href 【原文】https://www.toutiao.com/i6592365493435236872/ ?RPC概述 RPC(Remote Procedure Call)即遠程過程調用,也就是說兩臺服務器A,

深入理解Java中的底層阻塞原理實現

更多 安全 posix pla static events time() 方便 原理 談到阻塞,相信大家都不會陌生了。阻塞的應用場景真的多得不要不要的,比如 生產-消費模式,限流統計等等。什麽 ArrayBlockingQueue、 LinkedBlockingQueue、

Java併發(三):synchronized實現原理

一、synchronized用法 Java中的同步塊用synchronized標記。 同步塊在Java中是同步在某個物件上(監視器物件)。 所有同步在一個物件上的同步塊在同時只能被一個執行緒進入並執行操作。 所有其他等待進入該同步塊的執行緒將被阻塞,直到執行該同步塊中的執行緒退出。 (注:不要使用全

Java併發容器詳細介紹

概述         java.util包中的大部分容器都是非執行緒安全的,若要在多執行緒中使用容器,你可以使用Collections提供的包裝函式:synchronizedXXX,將普通容器變成執行緒安全的容器。但該方法僅僅是簡單地給容器使用同步,效率很

Java併發容器:ConcurrentLinkedQueue

轉自:https://blog.csdn.net/chenssy/article/details/74853120 http://cmsblogs.com/ 要實現一個執行緒安全的佇列有兩種方式:阻塞和非阻塞。阻塞佇列無非就是鎖的應用,而非阻塞則是CAS演算法的應用。下面我們就開始一個非

併發容器的原理,7大併發容器詳解、使用場景

併發容器的原理,7大併發容器詳解、及使用場景 http://youzhixueyuan.com/use-of-concurrent-containers.html 併發容器的由來 在Java併發程式設計中,經常聽到Java集合類,同步容器、併發容器,那麼他們有哪些具體分類,以及各自之間的

java併發系列java記憶體模型)

作為一個半路出家學java的菜菜菜鳥,真的是感覺路漫漫其修遠兮,工作間隙看了大約兩週的java併發,現在開始慢慢總結。 1.執行緒間的通訊 執行緒之間的協作主要有幾個點,wait,notify,notiyALl(),顯示Condition物件,佇列中的生產者消費者等等。主要就是條件佇

Java HashMap涉及的資料結構實現

提供的功能 基於雜湊表實現的Map; 非執行緒安全的Map實現; 鍵和值都可以為null(因為有處理null的情形); 基本操作get()和put()的時間消耗是固定的; 資料儲存結構會隨著HashMap的數量而變換成不同的資料結構。 涉及到的概念 預設初始化容量 最大容量 預設的負載係數(load f

Java併發程式設計()------併發程式設計面臨的問題

1. 併發程式設計面臨的問題 併發程式設計是為了讓程式執行得更快,但是並不是啟動更多的執行緒就能讓程式最大限度的併發執行,通常在併發程式設計中會遇到下面的問題. 1.1 上下文切換 CPU通過時間片非配演算法來迴圈執行任務,當前任務執行一個時間片後會切換到下一個任務.在切換時會儲存上一個任務的狀態,以便這個任

JAVA併發容器:ConcurrentSkipListMap

生活 目標定下來以後就不要去變,只要確定是對的,總可以到達。 二分查詢 二分查詢要求有序性,為了保障可以隨機訪問,因此會把資料儲存在連續的記憶體中,在查詢的時候效率高,但是在增加和刪除時需要大量移動元素以保證有序,所以效率不高。 如果需要快速的二分查詢,又要兼顧刪除增加元素的效率

JAVA併發容器:CopyOnWriteArrayList與CopyOnWriteArraySet

生活 所有的程式設計師都劇作家,而所有計算機都是糟糕的演員。 CopyOnWriteArrayList介紹 還記得學集合的時候,學的第一個集合就是ArrayList.它是一個由陣列實現的集合。因為他對陣列的增刪改和查詢都是不加鎖的,所以它並不是執行緒安全的。 因此,我們會引入到一

JAVA併發容器:JDK1.7 與 1.8 ConcurrentHashMap 區別

生活 為什麼我們總是沒有時間把事情做對,卻有時間做完它? 瞭解ConcurrentHashMap 工作中常用到hashMap,但是HashMap在多執行緒高併發場景下並不是執行緒安全的。 所以引入了ConcurrentHashMap,它是HashMap的執行緒安全版本,採用了分段

Java併發)——Thread類介紹 .md

1 序言 最近面試期間,發現自己的併發知識比較薄弱,準備寫一個關於併發的系列學習筆記。 2 Thread類主要方法 相信Thread類大家並不陌生,在建立執行緒的時候幾乎都會用到它。下面咱們聊一聊Thread類中的主要方法。 2.1 start方法 開啟一個執行