Java集合--ConcurrentHashMap原理
1.1 ConcurrentHashMap原始碼理解
上篇,介紹了ConcurrentHashMap的結構。本節中,我們來從原始碼的角度出發,來看下ConcurrentHashMap原理。
1.2 ConcurrentHashMap初始化
我們首先,來看下ConcurrentHashMap中的主要成員變數;
public class ConcurrentHashMap{ //用於根據給定的key的hash值定位到一個Segment final int segmentMask; //用於根據給定的key的hash值定位到一個Segment final int segmentShift; //HashEntry[]初始容量:決定了HashEntry陣列的初始容量和初始閥值大小 static final int DEFAULT_INITIAL_CAPACITY = 16; //Segment物件下HashEntry[]的初始載入因子: static final float DEFAULT_LOAD_FACTOR = 0.75f; //Segment物件下HashEntry[]最大容量: static final int MAXIMUM_CAPACITY = 1 [] segments; }
在ConcurrentHashMap中,定位到Segment[]中的某一角標,需要用到segmentMask和segmentShift這兩個屬性,他們的主要作用就是定位Segment[];
在上述屬性中,有的屬性是負責Segment[]的初始化,有的是負責HashEntry[]的初始化操作。如果單純靠屬性的名字來區分,還是很容易弄混淆的,這一點還要大家多多注意觀察,以及後續的分析。
DEFAULT_INITIAL_CAPACITY、DEFAULT_LOAD_FACTOR、MAXIMUM_CAPACITY與HashEntry[]的構建有關。
DEFAULT_CONCURRENCY_LEVEL、MIN_SEGMENT_TABLE_CAPACITY、MAX_SEGMENTS與Segment[]的構建有關。
下面,來看看ConcurrentHashMap的構造,它是如何初始化的!
public ConcurrentHashMap(int initialCapacity, float loadFactor, int concurrencyLevel) { //對容量、載入因子、併發等級做限制,不能小於(等於0) if (!(loadFactor > 0) || initialCapacity MAX_SEGMENTS) concurrencyLevel = MAX_SEGMENTS; //sshift用來記錄向左按位移動的次數 int sshift = 0; //ssize用來記錄segment陣列的大小 int ssize = 1; while (ssize MAXIMUM_CAPACITY) initialCapacity = MAXIMUM_CAPACITY; //c影響了每個Segment[]上要放置多少個HashEntry; int c = initialCapacity / ssize; if (c * ssize s0 = new Segment(loadFactor, (int)(cap * loadFactor), (HashEntry [])new HashEntry[cap]); //建立Segment[],指定segment陣列的長度: Segment [] ss = (Segment [])new Segment[ssize]; //使用CAS方式,將上面建立的segment物件放入segment[]陣列中; UNSAFE.putOrderedObject(ss, SBASE, s0); //對ConcurrentHashMap中的segment陣列賦值: this.segments = ss; }
首先,我們來普及下
x在上面的程式碼中,initialCapacity--初始容量大小,該引數影響著Segment物件下HashEntry[]的長度大小;loadFactor--載入因子,該引數影響著Segment物件下HashEntry[]陣列擴容閥值;concurrencyLevel--併發等級,該引數影響著Segment[]的長度大小。
在ConcurrentHashMap構造中,先是根據concurrencyLevel來計算出Segment[]的大小,而Segment[]的大小 就是大於或等於concurrencyLevel的最小的2的N次方。這樣的好處是是為了方便採用位運算來加速進行元素的定位。假如concurrencyLevel等於14,15或16,ssize都會等於16;
接下來,根據intialCapacity的值來確定Segment[]的大小,與計算Segment[]的方法一致。
值得一提的是,segmentShift和segmentMask這兩個屬性。上面說了,Segment[]長度就是2的N次方,在下面這段程式碼裡:
int sshift = 0; int ssize = 1;while (ssize這個N次方的N,就代表著sshift的大小,每while迴圈一次,sshift就增加1,那麼segmentShift的值就等於32減去n,而segmentMask就等於2的n次方減去1。
1.3 ConcurrentHashMap插入元素操作
在ConcurrentHashMap類中,使用put()最終呼叫的是Segment物件中的put()。
由於ConcurrentHashMap是執行緒安全的集合,所以在新增元素時,需要在操作時進行加鎖處理。
public V put(K key, V value) { Segments; //傳入的value不能為null if (value == null) throw new NullPointerException(); //計算key的hash值: int hash = hash(key); //透過key的hash值,定位ConcurrentHashMap中Segment[]的角標 int j = (hash >>> segmentShift) & segmentMask; //使用CAS方式,從Segment[]中獲取j角標下的Segment物件,並判斷是否存在: if ((s = (Segment )UNSAFE.getObject(segments, (j 在ConcurrentHashMap的put()中,首先需要透過key來定位到Segment[]的角標,然後在Segment中進行插入操作。
透過原始碼可以看到:定位Segment[]操作不但需要key的hash值,還需要使用到segmentShift、segmentMask屬性,前面提到過這兩個屬性的初始化是在ConcurrentHashMap中進行的。
Segment中插入元素方法:
//Segment類,繼承了ReentrantLock類:static final class Segmentextends ReentrantLock implements Serializable { //插入元素: final V put(K key, int hash, V value, boolean onlyIfAbsent) { //獲取鎖: HashEntry node = tryLock() ? null : scanAndLockForPut(key, hash, value); V oldValue; try { //獲取Segment物件中的 HashEntry[]: HashEntry [] tab = table; //計算key的hash值在HashEntry[]中的角標: int index = (tab.length - 1) & hash; //根據index角標獲取HashEntry物件: HashEntry first = entryAt(tab, index); //遍歷此HashEntry物件(連結串列結構): for (HashEntry e = first;;) { //判斷邏輯與HashMap大體相似: if (e != null) { K k; if ((k = e.key) == key || (e.hash == hash && key.equals(k))) { oldValue = e.value; if (!onlyIfAbsent) { e.value = value; ++modCount; } break; } e = e.next; } else { if (node != null) node.setNext(first); else node = new HashEntry (hash, key, value, first); int c = count + 1; if (c > threshold && tab.length 在Segment物件中,首先進行獲取鎖操作,也就是說在ConcurrentHashMap中,鎖是加到了每一個Segment物件上,而不是整個ConcurrentHashMap上。這樣的好處就是,當我們進行插入操作時,只要插入的不是同一個Segment物件,那麼併發執行緒就不需要進行等待操作,在保證安全的同時,又極大的提高了併發效能。
獲取鎖之後,透過hash值計算元素需要插入HashEntry[]的角標,再之後的操作基本與HashMap保持一致。
1.4 ConcurrentHashMap獲取元素操作
透過key,去獲取對應的value,大體邏輯與HashMap一致;
public V get(Object key) { Segments; HashEntry [] tab; //計算key的hash值: int h = hash(key); //計算該hash值所屬的Segment[]的角標: long u = (((h >>> segmentShift) & segmentMask) )UNSAFE.getObjectVolatile(segments, u)) != null && (tab = s.table) != null) { //再根據hash值,從Segment物件中的HashEntry[]獲取HashEntry物件:並進行連結串列遍歷 for (HashEntry e = (HashEntry ) UNSAFE.getObjectVolatile(tab, ((long)(((tab.length - 1) & h)) 在獲取操作中,獲取Segment物件和HashEntry物件,使用了不同的計算規則,其目的主要為了避免雜湊後的值一樣,儘可能將元素分散開來。
int h = hash(key) 計算Segment[]角標: (((h >>> segmentShift) & segmentMask)上面我們說過,Segment[]的大小為2的N次方,segmentShift屬性為32減去n,segmentMask屬性為2的n次方減去1。當我們假設都使用ConcurrentHashMap的預設值時候,Segment[]的大小為16,n為4,segmentShift位28,segmentMask位15。
則h無符號右移28位,剩餘4位有效值(高位補0)與segmentMask進行 &運算,得到Segment[]角標。
0000 0000 0000 0000 0000 0000 0000 XXXX 4位有效值 0000 0000 0000 0000 0000 0000 0000 1111 15的二進位制 ---------------------------------- &運算也就是根據元素的hash值的高n位就可以確定元素到底在哪一個Segment中。
與HashTable不同的是,ConcurrentHashMap在獲取元素時並沒有進行加鎖處理,那麼在併發場景下會不會產生資料隱患呢?
答案是NO!!!!
原因是,在ConcurrentHashMap的get()中,要獲取的元素被volatitle修飾符所修飾:HashEntry[]
static final class Segmentextends ReentrantLock implements Serializable { transient volatile HashEntry [] table; } 被volatile所修飾的變數,可以在多執行緒中保持可見性,可以執行同時讀的操作,並且保證不會讀到過期的值。當HashEntry物件被修改後,會立刻更新到記憶體中,並且使存在於CPU快取中的HashEntry物件過期無效,當其他執行緒進行讀取時,永遠都會讀取到記憶體中最新的值。
1.5 ConcurrentHashMap獲取長度操作
上面說完了put()和get(),本節在說說size()。與插入、獲取不同的是,size()有可能會對整個hash表進行加鎖處理。
public int size() { //得到所有的Segment[]: final Segment[] segments = this.segments; int size; boolean overflow; // true if size overflows 32 bits long sum; // sum of modCounts long last = 0L; // previous sum int retries = -1; // first iteration isn't retry try { for (;;) { //先比較在++,所以說能進到此邏輯中來,肯定retries大於2了 if (retries++ == RETRIES_BEFORE_LOCK) { //-1比較,變0 //0比較,變1 //1比較,變2 //2比較,變3 for (int j = 0; j seg = segmentAt(segments, j); if (seg != null) { //Segment物件被操作的次數: sum += seg.modCount; //Segment物件內元素的個數:也就是HashEntry物件的個數; int c = seg.count; //size每遍歷一次增加一次: if (c RETRIES_BEFORE_LOCK) { for (int j = 0; j 想要知道整個ConcurrentHashMap中的元素數量,就必須統計Segment物件下HashEntry[]中元素的個數。在Segment物件中有一個count屬性,它是負責記錄Segment物件中到底有多少個HashEntry的。當呼叫put()時,每增加一個元素,都會對count進行一次++,那麼是不是統計所有Segment物件中的count值就行了呢?
答案:不一定。
如果在遍歷Segment[]過程中,可能先遍歷的Segment進行了插入(刪除)操作,導致count發生了改變,引起整個統計結果不準確。所以最安全的做法就行是遍歷之前,將整個ConcurrentHashMap加鎖處理。
不過,整體加鎖的做法有失考慮,畢竟加鎖意味著效能下降,而ConcurrentHashMap的做法進行了一個折中處理。
我們思考下,在平常的工作場景,當我們對Map進行size()操作時,會有多大的機率,又同時進行插入(刪除)操作呢?
想必這個事情發生的可能還是很低的,那麼ConcurrentHashMap的作法是,連續遍歷2次Segment陣列,將count的值,進行相加操作。如果遍歷2次後的結果,都沒有變化,那麼就直接將count的和返回,如果此時發生的變化,那麼就對整張hash表進行加鎖處理。
這就是ConcurrentHashMap的處理方式,即保證了資料準確,又得到了效率!!
作者:賈博巖
連結:
來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/36/viewspace-2802568/,如需轉載,請註明出處,否則將追究法律責任。
相關文章
- Java 中 ConcurrentHashMap 原理分析JavaHashMap
- 帶你走進Java集合之ConcurrentHashMapJavaHashMap
- JAVA集合:ConcurrentHashMap深度解析(版本對比)JavaHashMap
- Java集合之ConcurrentHashMap原始碼淺析JavaHashMap原始碼
- 死磕 java集合之ConcurrentHashMap原始碼分析(一)JavaHashMap原始碼
- ConcurrentHashMap原理HashMap
- Java併發集合類ConcurrentHashMap底層核心原始碼解析JavaHashMap原始碼
- Java集合 ArrayList原理及使用Java
- ConcurrentHashMap底層原理HashMap
- 集合類HashMap,HashTable,ConcurrentHashMap區別?HashMap
- Java ConcurrentHashMap 高併發安全實現原理解析JavaHashMap
- 死磕 java集合之ConcurrentHashMap原始碼分析(二)——擴容全解析JavaHashMap原始碼
- 死磕 java集合之ConcurrentHashMap原始碼分析(一)——插入元素全解析JavaHashMap原始碼
- Java中的集合框架深度解析:從ArrayList到ConcurrentHashMap的效能考量Java框架HashMap
- ConcurrentHashMap的實現原理HashMap
- Java ConcurrentHashmap 解析JavaHashMap
- Java集合 HashSet的原理及常用方法Java
- Java集合 LinkedList的原理及使用Java
- Java 併發集合的實現原理Java
- Java集合詳解(二):ArrayList原理解析Java
- Java集合詳解(五):Hashtable原理解析Java
- Java集合詳解(三):HashMap原理解析JavaHashMap
- Java集合詳解(三):LinkedList原理解析Java
- Java併發5:ConcurrentHashMapJavaHashMap
- Java併發之ConcurrentHashMapJavaHashMap
- Java中ConcurrentHashMap學習JavaHashMap
- 死磕 java集合之ConcurrentHashMap原始碼分析(三)——刪除元素全解析(內含彩蛋)JavaHashMap原始碼
- HashMap、Hashtable、ConcurrentHashMap的原理與區別HashMap
- ConcurrentHashMap 實現原理和原始碼分析HashMap原始碼
- Java ConcurrentHashMap (Java程式碼實戰-005)JavaHashMap
- Java併發——ConcurrentHashMap(JDK 1.8)JavaHashMapJDK
- [Java併發]Concurrenthashmap的size()JavaHashMap
- Redis有序集合原理Redis
- Java集合類,從原始碼解析底層實現原理Java原始碼
- 【Java集合】1 集合概述Java
- 【Java集合原始碼剖析】Java集合框架Java原始碼框架
- 【JAVA集合】JAVA集合框架及其常用方法Java框架
- ConcurrentHashMap原始碼解析-Java7HashMap原始碼Java