小男孩‘自慰网亚洲一区二区,亚洲一级在线播放毛片,亚洲中文字幕av每天更新,黄aⅴ永久免费无码,91成人午夜在线精品,色网站免费在线观看,亚洲欧洲wwwww在线观看

分享

面試???--- CAS

 貪挽懶月 2022-06-20 發(fā)布于廣東

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問題。

  • 什么是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類了。

  • 解決ABA問題思路:

我們可以這個共享變量帶上一個版本號。比如現(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這個類了。

  • 帶時間戳的原子引用(AtomicStampedReference):
    這個時間戳就理解為版本號就行了??慈缦麓a:

 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問題?------ 使用帶時間戳的原子引用。

    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多