搜尋
首頁JavaJava面試題面試真題:請你聊聊並發中的CAS 機制

不知道同學們有沒有經歷過這樣的面試:

面試官:請你聊聊並發中的CAS機制吧
#小明:嗯,CAS是吧,好像聽過...我想想哈(大腦飛快思考)

 2分鐘過去了...
 空氣是死一般的沈靜.. .

面試官坐不住了,清了清喉嚨:咳... 那個,能簡單說說嗎?
小明憨憨一笑:嘿嘿,我好像忘記了...
面試官:哦,沒關係,今天的面試就到這吧,你回去等通知吧
小明垂頭喪氣地離開了...


別笑,小明其實是很多人的影子,在面試過程中民聊的同學不在少數,當然我也包括在內,其實這反映出一個很殘酷的現實:基礎不紮實!

那麼問題來了,如何在面試中吊打麵試官,穩如磐石?

學呀!光說有啥用,你得學啊,買的書你得看啊,買的課你得跟著練啊,別光打遊戲追劇了,想要變強,唯有禿頭!

現在是北京時間0:08,我在碼字寫文章,你呢?

面試真題:請你聊聊並發中的CAS 機制

一個小例子說說什麼是執行緒安全性

##並發是Java程式設計的基礎,在我們日常的工作中,很多時候都會跟並發打交道,當然,這也是面試考察的重點。在同時程式設計中,被提起最多的概念是

線程安全,下面我們先來看一段程式碼,看看運行後會發生什麼:

public class Test {
    private static int inc = 0;

    public static void main(String[] args) {
     // 设置栅栏,保证主线程能获取到程序各个线程全部执行完之后的值
        CountDownLatch countDownLatch = new CountDownLatch(1000000);
        // 设置100个线程同时执行
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
             // 循环10000次,对inc实现 +1 操作
                for (int j = 0; j < 10000; j++) {
                    inc++;
                    countDownLatch.countDown();
                }
            }).start();
        }
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        // 运行完毕,期望获取的结果是 1000000
        System.out.println("执行完毕,inc的值为:" + inc);
    }
}

程式中,我創建了100個線程,每個線程中對共享變數inc進行累加10000次的操作,如果是同步執行的話,inc最終的值應該是1000000,但我們知道在多執行緒中,程式是並發執行的,也就是說不同的執行緒可能會同時讀取到主記憶體相同的值,例如這樣的場景:

  • 執行緒A在某一個瞬間讀取了主記憶體的inc值為1000,它在自己的工作記憶體1,inc變成了1001;
  • 線程B在同樣的瞬間讀取到了主記憶體的inc值為1000,它也在自己的工作記憶體中對inc的值1, inc變成了1001;
  • 他們要往主記憶體寫入inc的值的時候並沒有做任何的同步控制,所以他們都有可能把自己工作內存的1001寫入到主內存;
  • 那麼很顯然主內存在進行兩次1 操作後,實際的結果只進行了一次1,變成了1001。

這就是一個很典型的多執行緒並發修改共享變數所帶來的問題,那麼很顯然,它的運行結果也如我們分析的那樣,某些情況下達不到1000000:

执行完毕,inc的值为:962370

有些人說透過volatile關鍵字可以解決這個問題,因為volatile可以保證執行緒之間的可見性,也就是說執行緒可以讀取到主記憶體最新的變數值,然後對其進行操作。

注意了,volatile只能保證線程的可見性,而不能保證線程操作的原子性,雖然線程讀取到了主記憶體的inc的最新值,但是讀取inc 1寫入主記憶體 是三步驟操作,所以volatile無法解決共享變數執行緒安全的問題。

那麼要如何解決這個問題? Java為我們提供下面幾種解決方案。

几种保证线程安全的方案

1. 通过synchronized关键字实现同步:

public class Test {
    private static int inc = 0;

    public static void main(String[] args) {
        // 设置栅栏,保证主线程能获取到程序各个线程全部执行完之后的值
        CountDownLatch countDownLatch = new CountDownLatch(1000000);
        // 设置100个线程同时执行
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                // 循环10000次,对inc实现 +1 操作
                for (int j = 0; j < 10000; j++) {
                 // 设置同步机制,让inc按照顺序执行
                    synchronized (Test.class) {
                        inc++;
                    }

                    countDownLatch.countDown();
                }
            }).start();
        }
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("执行完毕,inc的值为:" + inc);
    }
}

在上面的代码中,我们给 inc ++ 外面加了一层代码,使用 synchronized 设置类锁,保证了代码的同步执行,这是一种基于JVM自身的机制来保障线程的安全性,如果在并发量比较大的情况下,synchronized 会升级为重量级的锁,效率很低。synchronized无法获取当前线程的锁状态,发生异常的情况下会自动解锁,但是如果线程发生阻塞,它是不会释放锁的

执行结果:

执行完毕,inc的值为:1000000

可以看到,这种方式是可以保证线程安全的。

2. 通过Lock锁实现同步

public class Test {
    private static int inc = 0;
    private static Lock lock = new ReentrantLock();

    public static void main(String[] args) {
        // 设置栅栏,保证主线程能获取到程序各个线程全部执行完之后的值
        CountDownLatch countDownLatch = new CountDownLatch(1000000);

        // 设置100个线程同时执行
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                // 循环10000次,对inc实现 +1 操作
                for (int j = 0; j < 10000; j++) {
                 // 设置锁
                    lock.lock();
                    try {
                        inc++;
                    } finally {
                     // 解锁
                        lock.unlock();
                    }
                    countDownLatch.countDown();
                }
            }).start();
        }
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("执行完毕,inc的值为:" + inc);
    }
}

ReentrantLock的底层是通过AQS + CAS来实现的,在并发量比较小的情况下,它的性能不如 synchronized,但是随着并发量的增大,它的性能会越来越好,达到一定量级会完全碾压synchronized。并且Lock是可以尝试获取锁的,它通过代码手动去控制解锁,这点需要格外注意。

执行结果:

执行完毕,inc的值为:1000000

3. 使用 Atomic 原子类

public class Test {
    private static AtomicInteger inc = new AtomicInteger();

    public static void main(String[] args) {
        // 设置栅栏,保证主线程能获取到程序各个线程全部执行完之后的值
        CountDownLatch countDownLatch = new CountDownLatch(1000000);

        // 设置100个线程同时执行
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                // 循环10000次,对inc实现 +1 操作
                for (int j = 0; j < 10000; j++) {
                    inc.getAndAdd(1);
                    countDownLatch.countDown();
                }
            }).start();
        }
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("执行完毕,inc的值为:" + inc.get());
    }
}

AtomicInteger 底层是基于 CAS 的乐观锁实现的,CAS是一种无锁技术,相对于前面的方案,它的效率更高一些,在下面会详细介绍。

执行结果:

执行完毕,inc的值为:1000000

4. 使用 LongAdder 原子类

public class Test {
    private static LongAdder inc = new LongAdder();

    public static void main(String[] args) {
        // 设置栅栏,保证主线程能获取到程序各个线程全部执行完之后的值
        CountDownLatch countDownLatch = new CountDownLatch(1000000);

        // 设置100个线程同时执行
        for (int i = 0; i < 100; i++) {
            new Thread(() -> {
                // 循环10000次,对inc实现 +1 操作
                for (int j = 0; j < 10000; j++) {
                    inc.increment();
                    countDownLatch.countDown();
                }
            }).start();
        }
        try {
            countDownLatch.await();
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println("执行完毕,inc的值为:" + inc.intValue());
    }
}

LongAdder 原子类在 JDK1.8 中新增的类,其底层也是基于 CAS 机制实现的。适合于高并发场景下,特别是写大于读的场景,相较于 AtomicInteger、AtomicLong 性能更好,代价是消耗更多的空间,以空间换时间。

执行结果:

执行完毕,inc的值为:1000000

CAS理论

讲到现在,终于我们今天的主角要登场了,她就是CAS

CAS的意思是比较与交换(Compare And Swap),它是乐观锁的一种实现机制。

什么是乐观锁?通俗的来说就是它比较乐观,每次在修改变量的值之前不认为别的线程会修改变量,每次都会尝试去获得锁,如果获取失败了,它也会一直等待,直到获取锁为止。说白了,它就是打不死的小强。

而悲观锁呢,顾名思义,就比较悲观了,每次在修改变量前都会认为别人会动这个变量,所以它会把变量锁起来,独占,直到自己修改完毕才会释放锁。说白了,就是比较自私,把好东西藏起来自己偷偷享用,完事了再拿出来给别人。像之前的synchronized关键字就是悲观锁的一种实现。

CAS是一种无锁原子算法,它的操作包括三个操作数:需要读写的内存位置(V)、预期原值(A)、新值(B)。仅当 V值等于A值时,才会将V的值设为B,如果V值和A值不同,则说明已经有其他线程做了更新,则当前线程继续循环等待。最后,CAS 返回当前V的真实值。CAS 操作时抱着乐观的态度进行的,它总是认为自己可以成功完成操作。

CAS的实现

在Java中,JUC的atomic包下提供了大量基于CAS实现的原子类:

面試真題:請你聊聊並發中的CAS 機制

我们以AtomicInteger来举例说明。

AtomicInteger类内部通过一个Unsafe类型的静态不可变的变量unsafe来引用Unsafe的实例。

 // setup to use Unsafe.compareAndSwapInt for updates
private static final Unsafe unsafe = Unsafe.getUnsafe();

然后,AtomicInteger类用value保存自身的数值,并用get()方法对外提供。注意,它的value是使用volatile修饰的,保证了线程的可见性。

private volatile int value;

/**
 * Creates a new AtomicInteger with the given initial value.
 *
 * @param initialValue the initial value
 */
public AtomicInteger(int initialValue) {
    value = initialValue;
}

/**
 * Gets the current value.
 *
 * @return the current value
 */
public final int get() {
    return value;
}

一路跟踪incrementAndGet方法到的末尾可以看到是一个native的方法:

/**
 * Atomically increments by one the current value.
 *
 * @return the updated value
 */
public final int incrementAndGet() {
    return unsafe.getAndAddInt(this, valueOffset, 1) + 1;
}

//  getAndAddInt 方法
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;
}

// compareAndSet方法
public final native boolean compareAndSwapInt(Object var1, long var2, int var4, int var5);

可以看到其实incrementAndGet内部的原理就是通过compareAndSwapInt调用底层的机器指令不断比较内存旧值和期望的值,如果比较返回false就继续循环比较,如果返回true则将当前的新值赋给内存里的值,本次处理完毕。

由此我们知道,原子类实现的自增操作可以保证原子性的根本原因在于硬件(处理器)的相关指令支持。将语义上需要多步操作的行为通过一条指令来完成,CAS指令可以达到这个目的。

CAS的缺點

  • 作為樂觀鎖定的一種實現,當多執行緒競爭資源激烈的情況下,多個執行緒會發生自旋等待,會消耗一定的CPU資源。
  • CAS不可避免會出現ABA的問題,關於ABA問題的詮釋和解決方案,可以參考我的這篇文章:面試官問你:你知道什麼是ABA問題嗎?


好了,本期關於CAS的分享就到這裡結束了。 並發作為Java程式設計的基石,是一個非常重要的知識點,如果同學們對這塊的掌握比較薄弱,希望在讀完文章後能自己動手敲敲程式碼,思考一下什麼是CAS,有哪些優缺點,實作方式有哪些。 當然,並發是一個非常大的概念,這裡只是拋磚引玉,提到了其中的一個小知識點,給出了我自己學習的一點心得體會。 如果有闡述不到位或錯誤的地方,請私訊我一起討論,感謝!

我是程式設計師青戈,本期的面試問題分享到這裡就結束了,想提升自己,進階大工廠的同學一定要關注我的公眾號:Java學習指南,這裡每天會從實際的面試出發帶你學習和總結Java相關的知識,幫助你擴充技術棧,提昇個人實力。我們下期見~

以上是面試真題:請你聊聊並發中的CAS 機制的詳細內容。更多資訊請關注PHP中文網其他相關文章!

陳述
本文轉載於:Java学习指南。如有侵權,請聯絡admin@php.cn刪除

熱AI工具

Undresser.AI Undress

Undresser.AI Undress

人工智慧驅動的應用程序,用於創建逼真的裸體照片

AI Clothes Remover

AI Clothes Remover

用於從照片中去除衣服的線上人工智慧工具。

Undress AI Tool

Undress AI Tool

免費脫衣圖片

Clothoff.io

Clothoff.io

AI脫衣器

AI Hentai Generator

AI Hentai Generator

免費產生 AI 無盡。

熱門文章

R.E.P.O.能量晶體解釋及其做什麼(黃色晶體)
3 週前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.最佳圖形設置
3 週前By尊渡假赌尊渡假赌尊渡假赌
R.E.P.O.如果您聽不到任何人,如何修復音頻
3 週前By尊渡假赌尊渡假赌尊渡假赌
WWE 2K25:如何解鎖Myrise中的所有內容
4 週前By尊渡假赌尊渡假赌尊渡假赌

熱工具

記事本++7.3.1

記事本++7.3.1

好用且免費的程式碼編輯器

SecLists

SecLists

SecLists是最終安全測試人員的伙伴。它是一個包含各種類型清單的集合,這些清單在安全評估過程中經常使用,而且都在一個地方。 SecLists透過方便地提供安全測試人員可能需要的所有列表,幫助提高安全測試的效率和生產力。清單類型包括使用者名稱、密碼、URL、模糊測試有效載荷、敏感資料模式、Web shell等等。測試人員只需將此儲存庫拉到新的測試機上,他就可以存取所需的每種類型的清單。

DVWA

DVWA

Damn Vulnerable Web App (DVWA) 是一個PHP/MySQL的Web應用程序,非常容易受到攻擊。它的主要目標是成為安全專業人員在合法環境中測試自己的技能和工具的輔助工具,幫助Web開發人員更好地理解保護網路應用程式的過程,並幫助教師/學生在課堂環境中教授/學習Web應用程式安全性。 DVWA的目標是透過簡單直接的介面練習一些最常見的Web漏洞,難度各不相同。請注意,該軟體中

Dreamweaver CS6

Dreamweaver CS6

視覺化網頁開發工具

WebStorm Mac版

WebStorm Mac版

好用的JavaScript開發工具