CAS在底層源碼中是使用非常廣的,像我之前的HashMap源碼解析、volatile詳解等文章都有提到CAS。本文將詳細(xì)介紹CAS。
一、什么叫CAS?
CAS,是 compare and swap 的縮寫,即比較并交換。它是一種基于樂觀鎖的操作。它有三個操作數(shù),內(nèi)存值V,預(yù)期值A(chǔ),更新值B。當(dāng)且僅當(dāng)A和V相同時,才會把V修改成B,否則什么都不做。之前說到AtomicInteger用到了CAS,那么先從這個類說起。看如下代碼:
1public static void main(String[] args){
2 AtomicInteger atomicInteger = new AtomicInteger(5);
3 System.out.println(atomicInteger.compareAndSet(5,50));
4 System.out.println(atomicInteger.compareAndSet(5,100));
5}
AtomicInteger有一個compareAndSet方法,有兩個操作數(shù),第一個是期望值,第二個是希望修改成的值。首先初始值是5,第一次調(diào)用compareAndSet方法的時候,將5拷貝回自己的工作空間,然后改成50,寫回到主內(nèi)存中的時候,它期望主內(nèi)存中的值是5,而這時確實(shí)也是5,所以可以修改成功,主內(nèi)存中的值也變成了50,輸出true。第二次調(diào)用compareAndSet的時候,在自己的工作內(nèi)存將值修改成100,寫回去的時候,希望主內(nèi)存中的值是5,但是此時是50,所以set失敗,輸出false。這就是比較并交換,也即CAS。
二、CAS的工作原理
簡而言之,CAS工作原理就是UnSafe類和自旋鎖。
1、UnSafe類:
UnSafe類在jdk的rt.jar下面的一個類,全包名是sun.misc.UnSafe。這個類大多數(shù)方法都是native方法。由于Java不能操作計算機(jī)系統(tǒng),所以設(shè)計之初就留了一個UnSafe類。通過UnSafe類,Java就可以操作指定內(nèi)存地址的數(shù)據(jù)。調(diào)用UnSafe類的CAS,JVM會幫我們實(shí)現(xiàn)出匯編指令,從而實(shí)現(xiàn)原子操作?,F(xiàn)在就來分析一下AtomicInteger的getAndIncrement方法是怎么工作的??聪旅娴拇a:
1 public final int getAndIncrement() {
2 return unsafe.getAndAddInt(this, valueOffset, 1);
3 }
這個方法調(diào)用的是unsafe類的getAndAddInt方法,有三個參數(shù)。第一個表示當(dāng)前對象,也就是你new 的那個AtomicInteger對象;第二個表示內(nèi)存地址;第三個表示自增步伐。然后再點(diǎn)進(jìn)去看看這個getAndAddInt方法。
1public final int getAndAddInt(Object var1, long var2, int var4) {
2 int var5;
3 do {
4 var5 = this.getIntVolatile(var1, var2);
5 } while(!this.compareAndSwapInt(var1, var2, var5, var5 + var4));
6 return var5;
7 }
這里的val1就是當(dāng)前對象,val2是內(nèi)存地址,val4是1,也就是自增步伐。首先把當(dāng)前對象主內(nèi)存中的值賦給val5,然后進(jìn)入while循環(huán)。判斷當(dāng)前對象此刻主內(nèi)存中的值是否等于val5,如果是,就自增,否則繼續(xù)循環(huán),重新獲取val5的值。這里的compareAndSwapInt方法就是一個native方法,這個方法匯編之后是CPU原語指令,原語指令是連續(xù)執(zhí)行不會被打斷的,所以可以保證原子性。
2、自旋鎖:
所謂的自旋,其實(shí)就是上面getAndAddInt方法中的do while循環(huán)操作。當(dāng)預(yù)期值和主內(nèi)存中的值不等時,就重新獲取主內(nèi)存中的值,這就是自旋。
三、CAS的缺點(diǎn)
缺點(diǎn)有三個。
1、循環(huán)時間長,開銷大。
synchronized是加鎖,同一時間只能一個線程訪問,并發(fā)性不好。而CAS并發(fā)性提高了,但是由于CAS存在自旋操作,即do while循環(huán),如果CAS失敗,會一直進(jìn)行嘗試。如果CAS長時間不成功,會給CPU帶來很大的開銷。
2、只能保證一個共享變量的原子性。
上面也看到了,getAndAddInt方法的val1是代表當(dāng)前對象,所以它也就是能保證這一個共享變量的原子性。如果要保證多個,那只能加鎖了。
3、引來的ABA問題。
假設(shè)現(xiàn)在主內(nèi)存中的值是A,現(xiàn)有t1和t2兩個線程去對其進(jìn)行操作。t1和t2先將A拷貝回自己的工作內(nèi)存。這個時候t2線程將A改成B,刷回到主內(nèi)存。此刻主內(nèi)存和t2的工作內(nèi)存中的值都是B。接下來還是t2線程搶到執(zhí)行權(quán),t2又把B改回A,并刷回到主內(nèi)存。這時t1終于搶到執(zhí)行權(quán)了,自己工作內(nèi)存中的值的A,主內(nèi)存也是A,因此它認(rèn)為沒人修改過,就在工作內(nèi)存中把A改成了X,然后刷回主內(nèi)存。也就是說,在t1線程執(zhí)行前,t2將主內(nèi)存中的值由A改成B再改回A。這便是ABA問題??聪旅娴拇a演示(代碼涉及到原子引用,請參考下面的原子引用的介紹):
1class ABADemo {
2 static AtomicReference<String> atomicReference = new AtomicReference<>("A");
3 public static void main(String[] args){
4 new Thread(() -> {
5 atomicReference.compareAndSet("A","B");
6 atomicReference.compareAndSet("B","A");
7 },"t2").start();
8 new Thread(() -> {
9 try {
10 TimeUnit.SECONDS.sleep(1);
11 } catch (InterruptedException e) {
12 e.printStackTrace();
13 }
14 System.out.println(atomicReference.compareAndSet("A","C")
15 + "\t" + atomicReference.get());
16 },"t1").start();
17 }
18}
這段代碼執(zhí)行結(jié)果是"true C",這就證明了ABA問題的存在。如果一個業(yè)務(wù)只管開頭和結(jié)果,不管這個A中間是否變過,那么出現(xiàn)了ABA問題也沒事。如果需要A還是最開始的那個A,中間不許別人動手腳,那么就要規(guī)避ABA問題。要解決ABA問題,先看下面的原子引用的介紹。
JUC包下給我們提供了原子包裝類,像AtomicInteger。如果我不僅僅想要原子包裝類,我自己定義的User類也想具有原子操作,怎么辦呢?JUC為我們提供了AtomicReference,即原子引用??聪旅娴拇a:
1@AllArgsConstructor
2class User {
3 int age;
4 String name;
5
6 public static void main(String[] args){
7 User user = new User(20,"張三");
8 AtomicReference<User> atomicReference = new AtomicReference<>();
9 atomicReference.set(user);
10 }
11}
像這樣,就把User類變成了原子User類了。
我們可以這個共享變量帶上一個版本號。比如現(xiàn)在主內(nèi)存中的是A,版本號是1,然后t1和t2線程拷貝一份到自己工作內(nèi)存。t2將A改為B,刷回主內(nèi)存。此時主內(nèi)存中的是B,版本號為2。然后再t2再改回A,此時主內(nèi)存中的是A,版本號為3。這個時候t1線程終于來了,自己工作內(nèi)存是A,版本號是1,主內(nèi)存中是A,但是版本號為3,它就知道已經(jīng)有人動過手腳了。那么這個版本號從何而來,這就要說說AtomicStampedReference這個類了。
1class ABADemo {
2 static AtomicStampedReference<String> atomicReference = new AtomicStampedReference<>("A", 1);
3 public static void main(String[] args) {
4 new Thread(() -> {
5 try {
6 TimeUnit.SECONDS.sleep(1);// 睡一秒,讓t1線程拿到最初的版本號
7 } catch (InterruptedException e) {
8 e.printStackTrace();
9 }
10 atomicReference.compareAndSet("A", "B", atomicReference.getStamp(), atomicReference.getStamp() + 1);
11 atomicReference.compareAndSet("B", "A", atomicReference.getStamp(), atomicReference.getStamp() + 1);
12 }, "t2").start();
13 new Thread(() -> {
14 int stamp = atomicReference.getStamp();//拿到最開始的版本號
15 try {
16 TimeUnit.SECONDS.sleep(3);// 睡3秒,讓t2線程的ABA操作執(zhí)行完
17 } catch (InterruptedException e) {
18 e.printStackTrace();
19 }
20 System.out.println(atomicReference.compareAndSet("A", "C", stamp, stamp + 1));
21 }, "t1").start();
22 }
23}
初始版本號為1,t2線程每執(zhí)行一次版本號加。等t1線程執(zhí)行的時候,發(fā)現(xiàn)當(dāng)前版本號不是自己一開始拿到的1了,所以set失敗,輸出false。這就解決了ABA問題。
總結(jié):
1.什么是CAS? ------ 比較并交換,主內(nèi)存值和工作內(nèi)存值相同,就set為更新值。
2.CAS原理是什么?------ UnSafe類和自旋鎖。理解那個do while循環(huán)。
3.CAS缺點(diǎn)是什么?------ 循環(huán)時間長會消耗大量CPU資源;只能保證一個共享變量的原子性操作;造成ABA問題。
4.什么是ABA問題?------ t2線程先將A改成B,再改回A,此時t1線程以為沒人修改過。
5.如何解決ABA問題?------ 使用帶時間戳的原子引用。