天天看點

LongAdder類學習小結主要知識點:LongAdder類僞共享(False sharing)和cpu緩存行

longadder類

僞共享(false sharing)和cpu緩存行

longadder類是doug lea的傑作,jdk8中已把該類收錄在concurrent包下。

在多線程環境下,我們計數qps、某段時間調用錯誤量這類計算時,想到的是atomicinteger/atomiclong,在多線程情況下,這些能減少鎖帶來的性能損耗;但是在大并發,競争很激烈的情況下,出現cas不成功的情況也會帶來性能上的開銷。

longadder類主要為解決這個問題,分散計數值寫入時的壓力;主要原理就是利用分段寫人,減少競争。

比如qps值為10, 它可以分解為2+3+4+1,這幾個值分布在不同的段中單獨計數;當qps加1時,可以在任意一個段中加1即可,每個段綁定某個線程,每次更新值由任一段對應的線程來執行。這樣就很好的分散了線程之間的競争。

當要計算總量時,累加每個段中的值即可。

在小并發下,longadder與atomiclong更新效率差不多,但在高并發的場景下,longadder有着更高的吞吐量。 longadder實作比較複雜的,明顯的空間換時間。

總結下longadder減少沖突的方法以及在求和場景下比atomiclong更高效的原因:

開始和atomiclong一樣,都會先采用cas方式更新值

在初次cas方式失敗的情況下(通常證明多個線程同時想更新這個值),嘗試将這個值分隔成多個cell(sum的時候求和就好),讓這些競争的線程隻管更新自己所屬的cell,這樣就将競争壓力分散了。

longadder類繼承striped64類,看下striped64類的一個變量cells:

cell數組即為存儲分割後的每個long值;看下cell類的定義,

剛看到這個類代碼肯定會很奇怪,為什麼裡面定義了這些沒有用到的變量p0, p1, p2, p3, p4, p5, p6。這個就引出了僞共享(false sharing)和cpu緩存行;

cpu不是按單個bytes來讀取記憶體資料的,而是以“塊資料”的形式,每塊的大小通常為64bytes,這些“塊”被成為“cache line”(這種說法其實很不太正确,關于cache line的知識請參考文末的參考連結)

如果有兩個線程(thread1 和 thread2)同時修改一個volatile資料,把這個資料記為'x':

volatile long x;

如果線程1打算更改x的值,而線程2準備讀取:

thread1:x=3;

thread2: system.out.println(x);

由于x值被更新了,是以x值需要線上程1和線程2之間傳遞(從線程1到線程2),x的變更會引起整塊64bytes被交換,因為cpu核之間以cache lines的形式交換資料(cache lines的大小一般為64bytes)。有可能線程1和線程2在同一個核心裡處理,但是在這個簡單的例子中我們假設每個線程在不同的核中被處理。

我們知道long values的記憶體長度為8bytes,在我們例子中"cache line"為64bytes,是以一個cache line可以存儲8個long型變量,在cache line中已經存儲了一個long型變量x,我們假設cache line中剩餘的空間用來存儲了7個long型變量,例如從v1到v7

x,v1,v2,v3,v4,v5,v6,v7

一個cache lien可以被多個不同的線程所使用。如果有其他線程修改了v2的值,線程1和線程2将會強制重新加載cache line。你可以會疑惑我們隻是修改了v2的值不應該會影響其他變量,為啥線程1和線程2需要重新加載cache line呢。然後,即使對于多個線程來說這些更新操作是邏輯獨立的,但是一緻性的保持是以cache line為基礎的,而不是以單個獨立的元素。這種明顯沒有必要的共享資料的方式被稱作“false sharing”.

為了擷取一個cache line,核心需要執行幾百個指令。

如果核心需要等待一個cache line重新加載,核心将會停止做其他事情,這種現象被稱為"stall".stalls可以通過減少“false sharing”,一個減少"false sharing"的技巧是填充資料結構,使得線程操作的變量落入到不同的cache line中。

下面是一個填充了的資料結構的例子,嘗試着把x和v1放入到不同的cache line中

public class falsesharingwithpadding {

}

在你準備填充你的所有資料結構之前,你必須了解jvm會減少或者重排序沒有使用的字段,是以可能會重新引入“false sharing”。是以對象會在堆中的位置是沒有辦法保證的。

為了減少未使用的填充字段被優化掉的機會,将這些字段設定成為volatile會很有幫助。對于填充的建議是你隻需要在高度競争的并發類上使用填充,并且在你的目标架構上測試使用有很大提升之後采用填充。最好的方式是做10000玄幻疊代,消除jvm的實時優化的影響。

java 8中引入了一個新注解 @contented,主要是用來減少“false sharing”,在你需要避免“false sharing”的字段上标記注解,這可以暗示虛拟機“這個字段可以分離到不同的cache line中”,是以longadder在java8中的實作已經采用了@contended