面試必問的CAS,你懂了嗎?

概述linux

CAS(Compare-and-Swap),即比較並替換,是一種實現併發算法時經常使用到的技術,Java併發包中的不少類都使用了CAS技術。CAS也是如今面試常常問的問題,本文將深刻的介紹CAS的原理。面試

介紹CAS以前,咱們先來看一個例子。算法

上面這個例子在volatile關鍵字詳解文中用過,咱們知道,運行完這段代碼以後,並不會得到指望的結果,並且會發現每次運行程序,輸出的結果都不同,都是一個小於200000的數字。windows

經過分析字節碼咱們知道,這是由於volatile只能保證可見性,沒法保證原子性,而自增操做並非一個原子操做(以下圖所示),在併發的狀況下,putstatic指令可能把較小的race值同步回主內存之中,致使咱們每次都沒法得到想要的結果。那麼,應該怎麼解決這個問題了?緩存

解決方法:併發

首先咱們想到的是用synchronized來修飾increase方法。函數

使用synchronized修飾後,increase方法變成了一個原子操做,所以是確定能獲得正確的結果。可是,咱們知道,每次自增都進行加鎖,性能可能會稍微差了點,有更好的方案嗎?源碼分析


答案固然是有的,這個時候咱們可使用Java併發包原子操做類(Atomic開頭),例如如下代碼。性能

咱們將例子中的代碼稍作修改:race改爲使用AtomicInteger定義,「race++」改爲使用「race.getAndIncrement()」,AtomicInteger.getAndIncrement()是原子操做,所以咱們能夠確保每次均可以得到正確的結果,而且在性能上有不錯的提高(針對本例子,在JDK1.8.0_151下運行)。優化


經過方法調用,咱們能夠發現,getAndIncrement方法調用getAndAddInt方法,最後調用的是compareAndSwapInt方法,即本文的主角CAS,接下來咱們開始介紹CAS。

getAndAddInt方法解析:拿到內存位置的最新值v,使用CAS嘗試修將內存位置的值修改成目標值v+delta,若是修改失敗,則獲取該內存位置的新值v,而後繼續嘗試,直至修改爲功。


CAS是什麼?

CAS是英文單詞CompareAndSwap的縮寫,中文意思是:比較並替換。CAS須要有3個操做數:內存地址V,舊的預期值A,即將要更新的目標值B。

CAS指令執行時,當且僅當內存地址V的值與預期值A相等時,將內存地址V的值修改成B,不然就什麼都不作。整個比較並替換的操做是一個原子操做。


源碼分析

上面源碼分析時,提到最後調用了compareAndSwapInt方法,接着繼續深刻探討該方法,該方法在Unsafe中對應的源碼以下。

能夠看到調用了「Atomic::cmpxchg」方法,「Atomic::cmpxchg」方法在linux_x86和windows_x86的實現以下。

linux_x86的實現:


windows_x86的實現:


Atomic::cmpxchg方法解析:

mp是「os::is_MP()」的返回結果,「os::is_MP()」是一個內聯函數,用來判斷當前系統是否爲多處理器。

    若是當前系統是多處理器,該函數返回1。
    不然,返回0。

LOCK_IF_MP(mp)會根據mp的值來決定是否爲cmpxchg指令添加lock前綴。

    若是經過mp判斷當前系統是多處理器(即mp值爲1),則爲cmpxchg指令添加lock前綴。
    不然,不加lock前綴。

這是一種優化手段,認爲單處理器的環境沒有必要添加lock前綴,只有在多核狀況下才會添加lock前綴,由於lock會致使性能降低。cmpxchg是彙編指令,做用是比較並交換操做數。


intel手冊對lock前綴的說明以下:

    確保對內存的讀-改-寫操做原子執行。在Pentium及Pentium以前的處理器中,帶有lock前綴的指令在執行期間會鎖住總線,使得其餘處理器暫時沒法經過總線訪問內存。很顯然,這會帶來昂貴的開銷。從Pentium 4,Intel Xeon及P6處理器開始,intel在原有總線鎖的基礎上作了一個頗有意義的優化:若是要訪問的內存區域(area of memory)在lock前綴指令執行期間已經在處理器內部的緩存中被鎖定(即包含該內存區域的緩存行當前處於獨佔或以修改狀態),而且該內存區域被徹底包含在單個緩存行(cache line)中,那麼處理器將直接執行該指令。因爲在指令執行期間該緩存行會一直被鎖定,其它處理器沒法讀/寫該指令要訪問的內存區域,所以能保證指令執行的原子性。這個操做過程叫作緩存鎖定(cache locking),緩存鎖定將大大下降lock前綴指令的執行開銷,可是當多處理器之間的競爭程度很高或者指令訪問的內存地址未對齊時,仍然會鎖住總線。
    禁止該指令與以前和以後的讀和寫指令重排序。
    把寫緩衝區中的全部數據刷新到內存中。

上面的第1點保證了CAS操做是一個原子操做,第2點和第3點所具備的內存屏障效果,保證了CAS同時具備volatile讀和volatile寫的內存語義。


CAS的缺點:

CAS雖然很高效的解決了原子操做問題,可是CAS仍然存在三大問題。

    循環時間長開銷很大。
    只能保證一個共享變量的原子操做。
    ABA問題。

循環時間長開銷很大:

咱們能夠看到getAndAddInt方法執行時,若是CAS失敗,會一直進行嘗試。若是CAS長時間一直不成功,可能會給CPU帶來很大的開銷。


只能保證一個共享變量的原子操做:

當對一個共享變量執行操做時,咱們可使用循環CAS的方式來保證原子操做,可是對多個共享變量操做時,循環CAS就沒法保證操做的原子性,這個時候就能夠用鎖來保證原子性。


什麼是ABA問題?ABA問題怎麼解決?

若是內存地址V初次讀取的值是A,而且在準備賦值的時候檢查到它的值仍然爲A,那咱們就能說它的值沒有被其餘線程改變過了嗎?

若是在這段期間它的值曾經被改爲了B,後來又被改回爲A,那CAS操做就會誤認爲它歷來沒有被改變過。這個漏洞稱爲CAS操做的「ABA」問題。Java併發包爲了解決這個問題,提供了一個帶有標記的原子引用類「AtomicStampedReference」,它能夠經過控制變量值的版原本保證CAS的正確性。所以,在使用CAS前要考慮清楚「ABA」問題是否會影響程序併發的正確性,若是須要解決ABA問題,改用傳統的互斥同步可能會比原子類更高效。  

相關文章
相關標籤/搜索