ConcurrentHashMap薪缆,線程安全的HashMap,由于HashTable較重量級伞广,他會給整個加鎖拣帽,而ConcurrentHashMap只是給每個Segment加鎖,所以性能快很多嚼锄。
除了initialCapacity减拭、loadFactor之外,還有一個concurrentLevel屬性区丑,默認情況下拧粪,三個屬性分別為16,0.75沧侥,16
設置以上三個屬性后可霎,就得考慮鎖加在哪?并怎樣初始化加鎖的對象正什?
int sshift = 0;
int ssize = 1;
while(ssize < concurrentLevel){
++sshift;
ssize <<= 1;
}
上面這段代碼意思是:計算出一個不小于concurrentLevel的ssize值啥纸,而且它是2的n次方号杏。
默認情況下婴氮,ssize為16,根據這個參數傳入Segment的newArray方法盾致,創(chuàng)建大小為16的Segment數組
創(chuàng)建Segment數組后主经,數組元素對象怎么初始化?
int c = initialCapacity /ssize
if(c* ssize < initialCapacity){
++c;
}
int cap = 1;
while(cap < c){
cap << 1;
}
上面代碼意思是:用Map容量除以Segment數組大小庭惜,看每個Segment需要初始化多大罩驻,這里16/16=1,所以創(chuàng)建大小為cap=1的HashEntry[]數組护赊,將其賦給Segment惠遏,并且基于cap值和loadFactor計算threshold值砾跃。Segment繼承自ReentrantLock〗谒保可以發(fā)現抽高。一個Segment的數據結構就相當于HashMap(數組下有鏈表)
threshold = (int)(newTable.length * loadFactor)
put(key,value)
ConcurrentHashMap并沒有對整個方法加鎖(而HashTable對整個加鎖),和HashMap一樣透绩,首先對key.hashCode進行hash操作翘骂,得到hash值后計算其對應在數組中的哪個Segment對象。
return segments[(hash >>> segmentShift) & segmentMask]
找到數組中的Segment對象后帚豪,接著調用Segment的put方法完成操作碳竟,至此,才對其進行加鎖:lock狸臣,接著判斷當前存儲的對象個數加1后是否大于threshold莹桅,如大于,則rehash固棚,將當前HashEntry[]數組擴大2倍统翩,并重hash對象。
其余的操作跟HashMap差不多此洲,有則覆蓋厂汗,沒有則新創(chuàng)建HashEntry對象,放在鏈表頭部呜师。
HashEntry[] newTable = HashEntry.newArray(oldCapacity<<1)
get(key)
get操作只有在e.value == null的情況下娶桦,才會加lock再執(zhí)行一次e.value
問題:get操作大部分情況沒有l(wèi)ock,它是怎樣保證并發(fā)下數據的一致性的呢汁汗?
譬如1:在get找HashEntry鏈表過程中衷畦,這時候可能HashEntry[]數組會發(fā)生改變(put操作執(zhí)行),那它是如何讓保證的呢知牌?
答案就是因為HashEntry[]數組是volatile的祈争,當put改變數組后,get操作會立刻得到更新角寸。并且菩混,jdk5以后,volatile語義增強了扁藕,不僅僅保證數據的可見性沮峡,還能保證禁止在對象上的讀寫重排序,所以亿柑,在get時讀取到的HashEntry[]是最新的邢疙、并且構造已經完全的
譬如2:當get操作已經找到了HashEntry,準備開始遍歷鏈表了,這時HashEntry發(fā)生變化了怎么辦疟游?
答案就是HashEntry對象中的hash呼畸、key、next都是final的颁虐,value是volatile的役耕,這就意味著已獲取的HashEntry不會有next加入進來,而且value是可見的聪廉。
還有一個問題瞬痘,為什么要判斷e.value是否為null?而且如果為null再調用readValueUnderLock(HashEntry e)板熊?
以下為readValueUnderLock方法:
/**
* Reads value field of an entry under lock. Called if value
* field ever appears to be null. This is possible only if a
* compiler happens to reorder a HashEntry initialization with
* its table assignment, which is legal under memory model
* but is not known to ever occur.
*/
V readValueUnderLock(HashEntry e) {
lock();
try {
return e.value;
} finally {
unlock();
}
}
通過它的注釋框全,我們明白了,This is possible only if a compiler happens to reorder a HashEntry initialization with its table assignment,意思就是干签,只有在HashEntry初始化時出現指令重排津辩,才會導致該方法調用,并且也不確定是否發(fā)生容劳。
所以說喘沿,在JDK1.6里面,是弱一致性的竭贩,因為所有可見性都是以count實現的蚜印,當put和get并發(fā)時,get可能獲取不到最新的結果留量。而在1.7里面窄赋,會有UNSAFE.getObjectVolatile保證。