ConcurrentHashMap是如何提高並發時的吞吐性能



為並發吞吐性能所做的優化
ConcurrentHashMap使用了一些技巧來獲取高的並發性能,同時避免了鎖。這些技巧包括:

  • 為不同的Hash bucket(所謂hash bucket即不同范圍的key的hash值)使用多個寫鎖;
  • 利用JMM(Java Memory Model,java內存模型)的不確定性使得持有鎖的時間最小化,或者從根本上避免使用鎖。

ConcurrentHashMap為最常用的場景進行了優化,比如獲取一個已經存在於Map中的值。事實上,絕大多數成功的get()操作在運行中根本就沒有使用鎖,這是因為利用了JMM的不確定性。

多個寫鎖
回憶一下HashTable的線程安全是因為使用了一個單獨的全部Map范圍的鎖,這個鎖在所有的插入、刪除、查詢操作中都會持有,甚至在使用 Iterator遍歷整個Map時也會持有這個單獨的鎖。當鎖被一個線程持有時,就能夠防止其他線程訪問該Map,即便其他線程都處於閑置狀態。這種單個 鎖的機制極大的限制了並發的性能。

而ConcurrentHashMap拋棄了僅僅使用整個Map范圍的一個鎖的機制,取而代之的是使用了32個鎖,每個鎖會負責hash bucket的一個子集(即負責一部分key的hash值范圍)。而且這些鎖僅僅被那些更改Map內容的操作使用,比如put(), remove()。擁有32個單獨的鎖意味時最多可以有32個線程同時修改Map,這並不是說如果有少於32個線程同時修改Map而沒有線程被阻塞,32 僅僅是理論上的並發寫操作的極限,在實際中一般不一定會達到。在目前的軟硬件條件下,對多數程序而言,32個鎖總比一個鎖要好。

Map范圍的鎖
32個獨立的鎖,每個鎖負責hash bucket的一個子集,這意味着如果有些排他的訪問操作,就需要獲得全部的32個鎖,比如rehashing(即擴充hash bucket的數量,當HashMap的key增長時重新分布key元素)就必須是排他的訪問。但是JAVA語言本身沒有提供一種簡單的方式來獲取變長的 鎖,由於這種操作不會頻繁發生,那么ConcurrentHashMap是使用遞歸實現Map范圍的鎖。

JMM(Java內存模型)概覽
JMM就是Java內存模型,它定義了Java的線程之間如何通過內存進行交互。簡單說就是: JVM中存在一個主內存(Main Memory or Java Heap Memory),JAVA中所有變量都儲存在主存中,對於所有線程都是共享的。每個線程都有自己的工作內存(Working Memory),工作內存中保存的是主存中某些變量的拷貝,線程對所有變量的操作都是在工作內存中進行,線程之間無法相互直接訪問,變量傳遞均需要通過主 存完成。工作內存里的變量, 在多核CPU的情況下, 將大部分儲存於處理器高速緩存中, 高速緩存在不經過內存時, 所以線程之間也是不可見的。詳細的關於JMM的介紹會在后面的文章中。

大家都知道,CPU盡量使用自己的寄存器內部的數據,這樣會極大的提高性能。JAVA規范(JLS-Java Language Specification)中規定了一些內存操作不必立即被其他線程發現,並且還提供了兩個語言層面的機制來確保在多個線程之間保持內存操作的一致 性:synchronized和volatile。根據JSL中的描述"In the absence of explicit synchronization, an implementation is free to update the main memory in an order that may be surprising." (在沒有顯示的同步時,一個操作可以以一種令人驚訝的方式自由的更新主存。)這意思是說:沒有同步時,在一個線程中寫操作的操作順序也許和另外一個線程的 寫操作順序不一樣,並且更新內存變量會在不確定的時間之后被其他線程發現。

而使用synchronized的最根本的原因就是確保線程訪問關鍵代碼段的原子性。synchronized實際上提供了三個功能 atomicity, visibility, and ordering(原子性,可見性和順序行)。所謂原子性很好理解也很直接,就是確保不同線程再次進入同一區域的互斥性,防止在同一時刻有多於一個線程可 以訪問到被保護的代碼段。很不幸的是許多的文章只強調了synchronized的原子性方面,而不說其他兩個方面。在JMM中,同步扮演了一個重要的角 色,當獲取和釋放monitor(鎖)的時候,同步操作使得JVM執行了內存屏障(execute memory barriers)。

當一個線程獲取一個鎖時,它便執行了一次內存讀屏障(read barriers)——所謂內存屏障就是使得任何其他線程緩存的本地內存(CPU內部緩存或者CPU寄存器)無效,然后使得其他所有線程所在的CPU重新 從主存(內存)讀取這些變量的值。同樣,在釋放鎖的時候,所有其他線程均執行了一次內存寫屏障(write barriers)——Flush任何已經更改的變量到主存(內存)。互斥和內存屏障的結合意味着只要程序遵循正確的同步規則(這個同步規則就是:同步任 何被寫的變量會下次被另一個線程正確的讀;或者是同步任何讀一個下一次會被另一個線程正確的更改變量),每個線程都會看到它所使用的共享變量的正確值。

如果在訪問共享變量時沒有同步,你會碰到一些奇怪的事情,一些改變會很快的反應到其他線程里,而其他一些更改則需要花費一些時間才能反應到其他線 程中。這樣的結果就會使得你如果不用synchronized,那么你不能確保你看到一致的內存視圖(即相關變量在不同的線程中的值會不一致,也許有一些 值是臟數據)。通常的方法,也是推薦的方法去避免這些臟數據當然是正確采用synchronized。在這種情況下,比如在廣泛使用的基礎類 ConcurrentHashMap中就值得使用一些額外的專門知識和功夫去開發,以獲得高性能。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM