CAS和Atomic類參考:https://www.pdai.tech/md/java/thread/java-thread-x-juc-AtomicInteger.html#%E4%BD%BF%E7%94%A8%E4%B8%BE%E4%BE%8B
LongAdder類
LongAdder是jdk8新增的用于并發(fā)環(huán)境的計數(shù)器。雖然AtomicLong/AtomicInt使用了CAS操作在并發(fā)環(huán)境下避免了加鎖锁荔,當出現(xiàn)競爭CAS失敗時,AtomicLong/AtomicInt的邏輯很簡單,就是無限循環(huán)重復CAS敷硅,直到成功。在高并發(fā)環(huán)境下墓赴,如果有大量線程同時更新計數(shù)器竞膳,同一時間只有會一個線程更新成功,而其他線程都會因為更新失敗而空轉诫硕,大大降低了效率坦辟。而LongAdder維護了一個計數(shù)器數(shù)組,不同的線程可能更新計數(shù)數(shù)組中的不同元素章办,從而導致同一時間多個線程能夠成功更新計數(shù)器锉走,大大提高了高并發(fā)環(huán)境下的更新效率滨彻。本質上,LongAdder是一種以空間換時間的設計挪蹭。
成員變量:
base是一個長整型的變量亭饵,是基礎的計數(shù)器變量。當沒有線程競爭更新時梁厉,LongAdder使用base技術辜羊,此時的LongAdder與AtomicLong基本相同。
cells是計數(shù)器數(shù)組词顾,當出現(xiàn)多線程同時更新LongAdder時八秃,會使用cells計數(shù)。cells和它的成員cell使用的是懶加載肉盹,只有出現(xiàn)競爭真正需要使用數(shù)組時才會初始化昔驱。
cellsBusy是一個標志位,只有0和1兩個值上忍,用于實現(xiàn)CAS鎖骤肛。當LongAdder中一段代碼快需要互斥加鎖時,就調用casCellsBusy方法窍蓝,嘗試CAS將cellsBusy置1腋颠,失敗的線程就不進入代碼塊。成功進入代碼塊的線程執(zhí)行完畢后它抱,將cellsBusy置0秕豫,釋放鎖。
內部類Cell:
Cell類很簡單观蓄,就一個變量value用于計數(shù)混移,和一個cas方法使用UnSafe的CAS操作更新value值。在cell上有一個@Contended注解侮穿,它的作用是防止更新計數(shù)器時出現(xiàn)偽共享問題歌径。
[補充]JMM中的緩存行與偽共享問題:
簡單來說,計算機在CPU和內存之間使用了緩存技術亲茅,從而導致了可見性問題回铛。當計算機將數(shù)據(jù)從內存讀入緩存時,是以一個緩存行(64K)為單位讀入的克锣,這樣茵肃,當一個CPU更新了緩存行中的一個變量,計算機會將整個緩存行重新寫入內存袭祟,而其他CPU中該緩存行中所有變量都會失效验残。這就出現(xiàn)了當更新一個變量后同一緩存行的另一個變量在緩存中就失效了,讀取的時候需要重新在內存中讀巾乳,大大降低了效率您没。@Contended標簽通過在變量周圍填充空白鸟召,使一個緩存行中只有一個變量,解決了偽共享問題氨鹏。
具體參考:http://www.reibang.com/p/e338b550850f
如何確定cells數(shù)組中的哪個cell為當前線程計數(shù):
LongAdder會使用UnSafe初始化一個隨機變量保存到當前線程中欧募,之后每次使用UnSafe從當前線程中取出該變量對數(shù)組長度取余,得到該線程使用cells中哪個槽計數(shù)仆抵。從而將不同線程隨機分配到不同槽中計數(shù)跟继。
核心方法:
add方法?
將計數(shù)器增加x。邏輯如下:
情況1:當cells沒有初始化時(沒有線程競爭)镣丑,LongAdder使用base計數(shù)还栓,CAS修改base,若成功传轰,方法結束。
情況2:如果cells已經初始化了(出現(xiàn)了線程競爭)谷婆,找到當前線程在cells數(shù)組中的計數(shù)槽慨蛙,如果該槽中cell對象已經存在,CAS更新cell纪挎,若成功期贫,方法結束。
其他情況:進入longAccumulate方法异袄。
longAccumulate方法
longAccumulate代碼非常復雜通砍,根據(jù)不同的情況會有不同的處理流程。
情況1:因為CAS修改base失敗進入該方法烤蜕,此時cells為空封孙,longAccumulate創(chuàng)建cells數(shù)組和一個cell槽,使用cells數(shù)組來計數(shù)讽营。新建的cells數(shù)組長度只有2虎忌,它會在之后cell出現(xiàn)線程競爭時擴容。
注意橱鹏,為了線程安全膜蠢,保證只有一個線程創(chuàng)建cells數(shù)組,線程會先嘗試獲取cellsBusy鎖莉兰,如果已經有其他線程在創(chuàng)建cells挑围,即獲取鎖失敗,當前線程會再次嘗試使用base計數(shù)糖荒,如果依舊失敗杉辙,則循環(huán)longAccumulate方法。
情況2:該線程使用的cell對象還未初始化而進入該方法寂嘉。
LongAdder新建一個cell對象奏瞬,成功獲取鎖后枫绅,將cell放入cells數(shù)組中的位置。
情況3:線程CAS更新對應的cell時競爭失敗硼端,進入該方法并淋。
longAccumulate會先再嘗試一次CAS更新cell,如果依舊失敗珍昨,就會選擇擴容cells或者調用advanceProbe方法修改線程中的變量值县耽,使當前線程使用cells中的另一個cell來計數(shù)。是否擴容取決于cells長度是否超過cpu核心數(shù)镣典,如果超過兔毙,擴容將沒有意義。