前幾天刷朋友圈的時候,看到一段話: 如果現在我是傻逼,那麼我現在不管怎麼努力,也還是傻逼,因為我現在的傻逼是由以前決定的,現在努力,是為了讓以後的自己不再傻逼 。話糙理不糙,如果妄想現在努力一下,馬上就不再傻逼,那是不可能的,需要積累,需要沉澱,才能慢慢的不再傻逼。 好了,雞湯喝完。 今天我們的內容 ...
前幾天刷朋友圈的時候,看到一段話:如果現在我是傻逼,那麼我現在不管怎麼努力,也還是傻逼,因為我現在的傻逼是由以前決定的,現在努力,是為了讓以後的自己不再傻逼。話糙理不糙,如果妄想現在努力一下,馬上就不再傻逼,那是不可能的,需要積累,需要沉澱,才能慢慢的不再傻逼。
好了,雞湯喝完。
今天我們的內容是CAS以及原子操作類應用與源碼淺析,還會利用CAS來完成一個單例模式,還涉及到偽共用等。因為CAS是併發框架的基石,所以相當重要,這篇博客是一個長文,請做好準備。
說到CAS,不得不提到兩個專業詞語:悲觀鎖,樂觀鎖。我們先來看看什麼是悲觀鎖,什麼是樂觀鎖。
悲觀鎖,樂觀鎖
第一次看到悲觀鎖,樂觀鎖的時候,應該是在應付面試,看面試題的時候。有這麼一個例子:如何避免多線程對資料庫中的同一條記錄進行修改。
悲觀鎖
如果是mysql資料庫,利用for update關鍵字+事務。這樣的效果就是當A線程走到for update的時候,會把指定的記錄上鎖,然後B線程過來,就只能等待,A線程修改完數據之後,提交事務,鎖就被釋放了,這個時候B線程終於可以繼續做他的事情了。悲觀鎖往往是互斥的:只有我一個人可以進來,其他人都給我等著。這麼做是相當影響性能的。
樂觀鎖
在數據表中加一個版本號的欄位:version,這個欄位不需要程式員手動維護,是資料庫主動維護的,每次修改數據,version都會發生更改。
當version現在是1:
- A線程進來,讀到version是1。
- B線程進來,讀到version是1。
- A線程執行了更新的操作:update stu set name='codebear' where id=1 and version=1。成功。資料庫主動把version改成了2。
- B線程執行了更新的操作:update stu set name='hello' where id=1 and version=1。失敗。因為這個時候version欄位已經不是1了。
樂觀鎖其實不能叫鎖,它沒有鎖的概念。
在Java中,也有悲觀鎖,樂觀鎖的概念,悲觀鎖的典型代表就是Synchronized,而樂觀鎖的典型代表就是今天要說的CAS。而說CAS之前,先要說下原子操作類,因為CAS是原子操作類的基石,我們先要看看原子操作類的強大之處,從而產生探究CAS的興趣。
原子操作類的應用
我們先來看看原子操作類的應用。在Java中提供了很多原子操作類,比如AtomicInteger,其中有一個自增方法。
public class Main {
public static void main(String[] args) {
Thread[] threads = new Thread[20];
AtomicInteger atomicInteger = new AtomicInteger();
for (int i = 0; i < 20; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
atomicInteger.incrementAndGet();
}
});
threads[i].start();
}
join(threads);
System.out.println("x=" + atomicInteger.get());
}
private static void join(Thread[] threads) {
for (int i = 0; i < 20; i++) {
try {
threads[i].join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
運行結果:
這就是原子操作類的神奇之處了,在高併發的情況下,這種方法會比Synchronized更有優勢,畢竟Synchronized關鍵字會讓代碼串列化,失去了多線程優勢。
我們再來看個案例:
如果有一個需求,一個欄位的初始值為0,開三個線程:
- 一個線程執行:當x=0,x修改為100
- 一個線程執行:當x=100,x修改為50
- 一個線程執行:當x=50,x修改為60
public static void main(String[] args) {
AtomicInteger atomicInteger=new AtomicInteger();
new Thread(() -> {
if(!atomicInteger.compareAndSet(0,100)){
System.out.println("0-100:失敗");
}
}).start();
new Thread(() -> {
try {
Thread.sleep(500);////註意這裡睡了一會兒,目的是讓第三個線程先執行判斷的操作,從而讓第三個線程修改失敗
} catch (InterruptedException e) {
e.printStackTrace();
}
if(!atomicInteger.compareAndSet(100,50)){
System.out.println("100-50:失敗");
}
}).start();
new Thread(() -> {
if(!atomicInteger.compareAndSet(50,60)){
System.out.println("50-60:失敗");
}
}).start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
運行結果也是一樣的:
這個例子好像沒有什麼意思啊,甚至有點無聊,為什麼要舉這個例子呢,因為在這裡,我所調用的方法compareAndSet,首字母就是CAS,而且傳遞了兩個參數,這兩個參數是在原生CAS操作中必須要傳遞的,離原生的CAS操作更近一些。
既然原子操作類那麼牛逼,我們很有必要探究下原子操作類的基石:CAS。
CAS
CAS的全稱是Compare And Swap,即比較交換,當然還有一種說法:Compare And Set,調用原生CAS操作需要確定三個值:
- 要更新的欄位
- 預期值
- 新值
其中,要更新的欄位(變數)有時候會被拆分成兩個參數:1.實例 2.偏移地址。
也許你看到這裡,會覺得雲里霧裡,不知道我在說什麼,沒關係,繼續硬著頭皮看下去。
我們先來看看compareAndSet的源碼。
compareAndSet源碼淺析
首先,調用這個方法需要傳遞兩個參數,一個是預期值,一個是新值,這個預期值就相當於資料庫樂觀鎖版本號的概念,新值就是我們希望修改的值(是值,不是欄位)。我們來看看這個方法的內部實現:
public final boolean compareAndSet(int expect, int update) {
return unsafe.compareAndSwapInt(this, valueOffset, expect, update);
}
調用了unsafe下的compareAndSwapInt方法,除了傳遞了我們傳到此方法的兩個參數之外,又傳遞了兩個參數,這兩個參數就是我上面說的實例和偏移地址,this代表是當前類的實例,即AtomicInteger類的實例,這個偏移地址又是什麼鬼呢,說的簡單點,就是確定我們需要修改的欄位在實例的哪個位置。知道了實例,知道了我們的需要修改的欄位是在實例的哪個位置,就可以確定這個欄位了。不過,這個確定的過程不是在Java中做的,而是在更底層做的。
偏移地址是在本類的靜態代碼塊中獲得的:
private static final long valueOffset;
static {
try {
valueOffset = unsafe.objectFieldOffset
(AtomicInteger.class.getDeclaredField("value"));
} catch (Exception ex) { throw new Error(ex); }
}
unsafe.objectFieldOffset接收的是Field類型的參數,得到的就是對應欄位的偏移地址了,這裡就是獲得value欄位在本類,即AtomicInteger中的偏移地址。
我們在來看看value欄位的定義:
private volatile int value;
volatile是為了保證記憶體的可見性。
大家肯定想一探究竟compareAndSwapInt和objectFieldOffset這兩個方法中做了什麼事情,很遺憾,個人水平有限,目前還沒有能力去探究,只知道這種寫法是JNI,會調用到C或者C++,最終會把對應的指令發送給CPU,這是可以保證原子性的。
我們可以看下這兩個方法的定義:
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);
public native long objectFieldOffset(Field var1);
這兩個方法被native標記了。
我們來為compareAndSwapInt方法做一個比較形象的解釋:
當我們執行compareAndSwapInt方法,傳入10和100,Java會和更底層進行通信:老鐵,我給你了欄位的所屬實例和偏移地址,你幫我看下這個欄位的值是不是10,如果是10的話,你就改成100,並且返回true,如果不是的話,不用修改,返回false把。
其中比較的過程就是compare,修改的值的過程就是swap,因為是把舊值替換成新值,所以我們把這樣的操作稱為CAS。
我們再來看看incrementAndGet的源碼。
incrementAndGet源碼淺析
public final int incrementAndGet() {
return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}
public final int getAndAddInt(Object var1, long var2, int var4) {
int var5;
do {
var5 = this.getIntVolatile(var1, var2);
} while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
return var5;
}
incrementAndGet方法會調到用getAndAddInt方法,這裡有三個參數:
- var1:實例。
- var2:偏移地址。
- var4:需要自增的值,這裡是1。
getAndAddInt方法內部有一個while迴圈,迴圈體內部根據實例和偏移地址獲得對應的值,這裡先稱為A,再來看看while裡面的判斷內容,JDK和更底層進行通訊:嘿,我把實例和偏移地址給你,你幫我看下這個值是不是A,如果是的話,幫我修改成A+1,返回true,如果不是的話,返回false吧。
這裡要思考一個問題:為什麼需要while迴圈?
比如同時有兩個線程執行到了getIntVolatile方法,拿到的值都是10,其中線程A執行native方法,修改成功,但是線程B就修改失敗了啊,因為CAS操作是可以保證原子性的,所以線程B只能苦逼的再一次迴圈,這一次拿到的值是11,又去執行native方法,修改成功。
像這樣的while迴圈,有一個高大上的稱呼:CAS自旋。
讓我們試想一下,如果現在併發真的很高很高,會出現什麼事情?大量的線程在進行CAS自旋,這太浪費CPU了吧。所以在Java8之後,對原子操作類進行了一定的優化,這個我們後面再說。
可能大家對於原子操作類的底層實現,還是比較迷茫,還是不知道unsafe下麵的方法到底是什麼意思,畢竟剛纔只是簡單的讀了下代碼,俗話說“紙上得來終覺淺,絕知此事要躬行”,所以我們需要自己調用下unsafe下麵的方法,來加深理解。
Unsafe
Unsafe:不安全的,既然有這樣的命名,說明這個類是比較危險的,Java官方也不推薦我們直接操作Unsafe類,但是畢竟現在是學習階段,寫寫demo而已,只要不是發佈到生產環境,又有什麼關係呢?
Unsafe下麵的方法還是比較多的,我們選擇幾個方法來看下,最終我們會利用這幾個方法來完成一個demo。
objectFieldOffset:接收一個Field類型的數據,返回偏移地址。
compareAndSwapInt:比較交換,接收四個參數:實例,偏移地址,預期值,新值。
getIntVolatile:獲得值,支持Volatile,接收兩個參數:實例,偏移地址。
這三個方法在上面的源碼淺析中,已經出現過了,也進行了一定的解釋,這裡再解釋一下,就是為了加深印象,我在學習CAS的時候,也是反覆的看博客,看源碼,突然恍然大悟。我們需要用這三個方法來完成一個demo:寫一個原子操作自增的方法,自增的值可以自定義,沒錯,這個方法上面我已經分析過了。下麵直接放出代碼:
public class MyAtomicInteger {
private volatile int value;
private static long offset;//偏移地址
private static Unsafe unsafe;
static {
try {
Field theUnsafeField = Unsafe.class.getDeclaredField("theUnsafe");
theUnsafeField.setAccessible(true);
unsafe = (Unsafe) theUnsafeField.get(null);
Field field = MyAtomicInteger.class.getDeclaredField("value");
offset = unsafe.objectFieldOffset(field);//獲得偏移地址
} catch (Exception e) {
e.printStackTrace();
}
}
public void increment(int num) {
int tempValue;
do {
tempValue = unsafe.getIntVolatile(this, offset);//拿到值
} while (!unsafe.compareAndSwapInt(this, offset, tempValue, value + num));//CAS自旋
}
public int get() {
return value;
}
}
public class Main {
public static void main(String[] args) {
Thread[] threads = new Thread[20];
MyAtomicInteger atomicInteger = new MyAtomicInteger();
for (int i = 0; i < 20; i++) {
threads[i] = new Thread(() -> {
for (int j = 0; j < 1000; j++) {
atomicInteger.increment(1);
}
});
threads[i].start();
}
for (int i = 0; i < threads.length; i++) {
try {
threads[i].join();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
System.out.println("x=" + atomicInteger.get());
}
}
運行結果:
你可能會有疑問,為什麼需要用反射來獲取theUnsafe,其實這是JDK為了保護我們,讓我們無法方便的獲得unsafe,如果我們和JDK一樣來獲得unsafe會報錯:
@CallerSensitive
public static Unsafe getUnsafe() {
Class var0 = Reflection.getCallerClass();
if (!VM.isSystemDomainLoader(var0.getClassLoader())) {
throw new SecurityException("Unsafe");//如果我們也以getUnsafe來獲得theUnsafe,會拋出異常
} else {
return theUnsafe;
}
}
CAS與單例模式
對的,你沒看錯,我也沒寫錯,用CAS也可以完成單例模式,雖然在正常開發中,不會有人用CAS來完成單例模式,但是是檢驗是否學會CAS的一個很好的題目。
public class Singleton {
private Singleton() {
}
private static AtomicReference<Singleton> singletonAtomicReference = new AtomicReference<>();
public static Singleton getInstance() {
while (true) {
Singleton singleton = singletonAtomicReference.get();// 獲得singleton
if (singleton != null) {// 如果singleton不為空,就返回singleton
return singleton;
}
// 如果singleton為空,創建一個singleton
singleton = new Singleton();
// CAS操作,預期值是NULL,新值是singleton
// 如果成功,返回singleton
// 如果失敗,進入第二次迴圈,singletonAtomicReference.get()就不會為空了
if (singletonAtomicReference.compareAndSet(null, singleton)) {
return singleton;
}
}
}
}
註釋寫的已經比較清楚了,可以對著註釋,再好好理解一下。
ABA
compareAndSet方法,上面已經寫過一個demo,大家可以也試著分析下源碼,我就不再分析了,我之所以要再次提到compareAndSet方法,是為了引出一個問題。
假設有三個步驟:
- 修改150為50
- 修改50為150
- 修改150為90
請仔細看,這三個步驟做的事情,一個變數剛開始是150,修改成了50,後來又被修改成了150!(又改回去了),最後如果這個變數是150,再改成90。這就是CAS中ABA的問題。
第三步,判斷這個值是否是150,有兩種不同的需求:
- 沒錯啊,雖然這個值被修改了,但是現在被改回去了啊,所以第三步的判斷是成立的。
- 不對,這個值雖然是150,但是這個值曾經被修改過,所以第三步的判斷是不成立的。
針對於第二個需求,我們可以用AtomicStampedReference來解決這個問題,AtomicStampedReference支持泛型,其中有一個stamp的概念。下麵直接貼出代碼:
public static void main(String[] args) {
try {
AtomicStampedReference<Integer> atomicStampedReference = new AtomicStampedReference<Integer>(150, 0);
Thread thread1 = new Thread(() -> {
Integer oldValue = atomicStampedReference.getReference();
int stamp = atomicStampedReference.getStamp();
if (atomicStampedReference.compareAndSet(oldValue, 50, 0, stamp + 1)) {
System.out.println("150->50 成功:" + (stamp + 1));
}
});
thread1.start();
Thread thread2 = new Thread(() -> {
try {
Thread.sleep(1000);//睡一會兒,是為了保證線程1 執行完畢
} catch (InterruptedException e) {
e.printStackTrace();
}
Integer oldValue = atomicStampedReference.getReference();
int stamp = atomicStampedReference.getStamp();
if (atomicStampedReference.compareAndSet(oldValue, 150, stamp, stamp + 1)) {
System.out.println("50->150 成功:" + (stamp + 1));
}
});
thread2.start();
Thread thread3 = new Thread(() -> {
try {
Thread.sleep(2000);//睡一會兒,是為了保證線程1,線程2 執行完畢
} catch (InterruptedException e) {
e.printStackTrace();
}
Integer oldValue = atomicStampedReference.getReference();
int stamp = atomicStampedReference.getStamp();
if (atomicStampedReference.compareAndSet(oldValue, 90, 0, stamp + 1)) {
System.out.println("150->90 成功:" + (stamp + 1));
}
});
thread3.start();
thread1.join();
thread2.join();
thread3.join();
System.out.println("現在的值是" + atomicStampedReference.getReference() + ";stamp是" + atomicStampedReference.getStamp());
} catch (Exception e) {
e.printStackTrace();
}
}
Java8對於原子操作類的優化
在進行incrementAndGet源碼解析的時候,說到一個問題:在高併發之下,N多線程進行自旋競爭同一個欄位,這無疑會給CPU造成一定的壓力,所以在Java8中,提供了更完善的原子操作類:LongAdder。
我們簡單的說下它做了下什麼優化,它內部維護了一個數組Cell[]和base,Cell裡面維護了value,在出現競爭的時候,JDK會根據演算法,選擇一個Cell,對其中的value進行操作,如果還是出現競爭,會換一個Cell再次嘗試,最終把Cell[]裡面的value和base相加,得到最終的結果。
因為其中的代碼比較複雜,我就選擇幾個比較重要的問題,帶著問題去看源碼:
- Cell[]是何時被初始化的。
- 如果沒有競爭,只會對base進行操作,這是從哪裡看出來的。
- 初始化Cell[]的規則是什麼。
- Cell[]擴容的時機是什麼。
- 初始化Cell[]和擴容Cell[]是如何保證線程安全性的。
這是LongAdder類的UML圖:
add方法:
public void add(long x) {
Cell[] cs; long b, v; int m; Cell c;
if ((cs = cells) != null || !casBase(b = base, b + x)) {//第一行
boolean uncontended = true;
if (cs == null || (m = cs.length - 1) < 0 ||//第二行
(c = cs[getProbe() & m]) == null ||//第三行
!(uncontended = c.cas(v = c.value, v + x)))//第四行
longAccumulate(x, null, uncontended);//第五行
}
}
第一行:
||判斷,前者是判斷cs=cells是否【不為空】,後者是判斷CAS是否【不成功】 。
casBase做什麼了?
final boolean casBase(long cmp, long val) {
return BASE.compareAndSet(this, cmp, val);
}
這個比較簡單,就是調用compareAndSet方法,判斷是否成功:
- 如果當前沒有競爭,返回true。
- 如果當前有競爭,有線程會返回false。
再回到第一行,整體解釋下這個判斷:如果cell[]已經被初始化了,或者有競爭,才會進入到第二行代碼。如果沒有競爭,也沒有初始化,就不會進入到第二行代碼。
這就回答了第二個問題:如果沒有競爭,只會對base進行操作,是從這裡看出來的。
第二行代碼:
||判斷,前者判斷cs是否【為NULL】,後者判斷(cs的長度-1)是否【大於0】。這兩個判斷,應該都是判斷Cell[]是否初始化的。如果沒有初始化,會進入第五行代碼。
第三行代碼:
如果cell進行了初始化,通過【getProbe() & m】演算法得到一個數字,判斷cs[數字]是否【為NULL】,並且把cs[數字]賦值給了c,如果【為NULL】,會進入第五行代碼。
我們需要簡單的看下getProbe() 中做了什麼:
static final int getProbe() {
return (int) THREAD_PROBE.get(Thread.currentThread());
}
private static final VarHandle THREAD_PROBE;
我們只要知道這個演算法是根據THREAD_PROBE算出來的即可。
第四行代碼:
對c進行了CAS操作,看是否成功,並且把返回值賦值給uncontended,如果當前沒有競爭,就會成功,如果當前有競爭,就會失敗,在外面有一個!(),所以CAS失敗了,會進入第五行代碼。需要註意的是,這裡已經是對Cell元素進行操作了。
第五行代碼:
這方法內部非常複雜,我們先看下方法的整體:
有三個if:
1.判斷cells是否被初始化了,如果被初始化了,進入這個if。
這裡面又包含了6個if,真可怕,但是在這裡,我們不用全部關註,因為我們的目標是解決上面提出來的問題。
我們還是先整體看下:
第一個判斷:根據演算法,拿出cs[]中的一個元素,並且賦值給c,然後判斷是否【為NULL】,如果【為NULL】,進入這個if。
if (cellsBusy == 0) { // 如果cellsBusy==0,代表現在“不忙”,進入這個if
Cell r = new Cell(x); //創建一個Cell
if (cellsBusy == 0 && casCellsBusy()) {//再次判斷cellsBusy ==0,加鎖,這樣只有一個線程可以進入這個if
//把創建出來Cell元素加入到Cell[]
try {
Cell[] rs; int m, j;
if ((rs = cells) != null &&
(m = rs.length) > 0 &&
rs[j = (m - 1) & h] == null) {
rs[j] = r;
break done;
}
} finally {
cellsBusy = 0;//代表現在“不忙”
}
continue; // Slot is now non-empty
}
}
collide = false;
這就對第一個問題進行了補充,初始化Cell[]的時候,其中一個元素是NULL,這裡對這個為NULL的元素進行了初始化,也就是只有用到了這個元素,才去初始化。
第六個判斷:判斷cellsBusy是否為0,並且加鎖,如果成功,進入這個if,對Cell[]進行擴容。
try {
if (cells == cs) // Expand table unless stale
cells = Arrays.copyOf(cs, n << 1);
} finally {
cellsBusy = 0;
}
collide = false;
continue;
這就回答了第五個問題的一半:擴容Cell[]的時候,利用CAS加了鎖,所以保證線程的安全性。
那麼第四個問題呢?首先你要註意,最外面是一個for (;;)死迴圈,只有break了,才終止迴圈。
一開始collide為false,在第三個if中,對cell進行CAS操作,如果成功,就break了,所以我們需要假設它是失敗的,進入第四個if,第四個if中會判斷Cell[]的長度是否大於CPU核心數, 如果小於核心數,會進入第五個判斷,這個時候collide為false,會進入這個if,把collide改為true,代表有衝突,然後跑到advanceProbe方法,生成一個新的THREAD_PROBE,再次迴圈。如果在第三個if中,CAS還是失敗,再次判斷Cell[]的長度是否大於核心數,如果小於核心數,會進入第五個判斷,這個時候collide為true,所以不會進入第五個if中去了,這樣就進入了第六個判斷,進行擴容。是不是很複雜。
簡單的來說,Cell[]擴容的時機是:當Cell[]的長度小於CPU核心數,並且已經兩次Cell CAS失敗了。
2.前面兩個判斷很好理解,主要看第三個判斷:
final boolean casCellsBusy() {
return CELLSBUSY.compareAndSet(this, 0, 1);
}
cas設置CELLSBUSY為1,可以理解為加了個鎖,因為馬上就要進行初始化了。
try { // Initialize table
if (cells == cs) {
Cell[] rs = new Cell[2];
rs[h & 1] = new Cell(x);
cells = rs;
break done;
}
} finally {
cellsBusy = 0;
}
初始化Cell[],可以看到長度為2,根據演算法,對其中的一個元素進行初始化,也就是此時Cell[]的長度為2,但是裡面有一個元素還是NULL,現在只是對其中一個元素進行了初始化,最終把cellsBusy修改成了0,代表現在“不忙了”。
這就回答了
第一個問題:當出現競爭,且Cell[]還沒有被初始化的時候,會初始化Cell[]。
第四個問題:初始化的規則是創建長度為2的數組,但是只會初始化其中一個元素,另外一個元素為NULL。
第五個問題的一半:在對Cell[]進行初始化的時候,是利用CAS加了鎖,所以可以保證線程安全。
3.如果上面的都失敗了,對base進行CAS操作。
如果大家跟著我一起在看源碼,會發現一個可能以前從來也沒有見過的註解:
這個註解是乾什麼的?Contended是用來解決偽共用的。
好了,又引出來一個知識盲區,偽共用為何物。
偽共用
我們知道CPU和記憶體之間的關係:當CPU需要一個數據,會先去緩存中找,如果緩存中沒有,會去記憶體找,找到了,就把數據複製到緩存中,下次直接去緩存中取出即可。
但是這種說法,並不完善,在緩存中的數據,是以緩存行的形式存儲的,什麼意思呢?就是一個緩存行可能不止一個數據。假如一個緩存行的大小是64位元組,CPU去記憶體中取數據,會把臨近的64位元組的數據都取出來,然後複製到緩存。
這對於單線程,是一種優化。試想一下,如果CPU需要A數據,把臨近的BCDE數據都從記憶體中取出來,並且放入緩存了,CPU如果再需要BCDE數據,就可以直接去緩存中取了。
但在多線程下就有劣勢了,因為同一緩存行的數據,同時只能被一個線程讀取,這就叫偽共用了。
有沒有辦法可以解決這問題呢?聰明的開發者想到了一個辦法:如果緩存行的大小是64位元組,我可以加上一些冗餘欄位來填充到64位元組。
比如我只需要一個long類型的欄位,現在我再加上6個long類型的欄位作為填充,一個long占8位元組,現在是7個long類型的欄位,也就是56位元組,另外對象頭也占8個位元組,正好64位元組,正好夠一個緩存行。
但是這種辦法不夠優雅,所以在Java8中推出了@jdk.internal.vm.annotation.Contended註解,來解決偽共用的問題。但是如果開發者想用這個註解, 需要添加 JVM 參數,具體參數我在這裡就不說了,因為我沒有親測過。
這一章的篇幅相當長,幾乎涵蓋了CAS中大部分常見的問題。
併發框架,是非常難學的,因為在開發中,很少會真正用到併發方面的知識,但是併發對於提高程式的性能,吞吐量是非常有效的手段,所以併發是值得花時間去學習,去研究的。