Java高性能編程之CAS與ABA及解決方法

Java高性能編程之CAS與ABA及解決方法

前言

若是喜歡暗色調的界面或者想換換界面,能夠看看我在我的博客發佈的 Java高性能編程之CAS與ABA及解決方法html

CAS概念

CAS,全稱Compare And Swap,比較與交換。java

屬於硬件級別的同步原語,從處理器層面提供了內存操做的原子性。node

從概念上,咱們能夠得出三點。第一,CAS的運做方式(經過比較與交換實現)。第二,硬件層面支持,性能確定不低(固然它也不是銀彈)。第三,提供原子性,那麼它的功能確定是確保原子性,從而確保線程安全。編程

實際使用中,CAS操做須要輸入兩個數值,一箇舊值A(指望操做前的值)和一個新值B,在操做期間先將舊值A與實際內存中的值進行比較,若是沒有發生變化,纔將實際內存中的值交換成新值B,若是發生了變化則不交換。數組

CAS應用場景

既然CAS的功能是提供原子性,那麼從這個角度出發思考,如計數器,帳戶轉帳等。安全

那麼提到計數器,就不得不提到JUC包下的atomic包了。其中提供了大量原子操做了,如Integer類型的值變化,Long類型的值變化,Boolean類型的值變化。多線程

說到這裏,某些人就要一句「球多麻袋」,Integer類型的值變化,不就一句代碼嘛(如i = i + 1;),不就是原子操做嘛。即便有賦值操做,也能夠寫成(i++;),這樣不就一個操做了嘛。固然學習過彙編或對計算機指令有必定了解的朋友可能就知道緣由了。不少時候,咱們在程序中的一段代碼,編譯到底層執行時,每每是多個語句(誰讓CPU只能執行很是簡單的操做呢)。如i++操做編譯成Java指令後是如下四句:架構

  1. getfield
  2. iconst_1
  3. iadd
  4. putfield

具體的意思,我就不解讀了(不是今天的重點),感興趣的,能夠百度或者@我。ide

話題回到JUC包下的atomic包,我之因此提它,就是由於其原子性的實現就是依靠CAS實現的。函數

AtomicInteger類:

/**
     * Atomically sets to the given value and returns the old value.
     *
     * @param newValue the new value
     * @return the previous value
     */
    public final int getAndSet(int newValue) {
        return unsafe.getAndSetInt(this, valueOffset, newValue);
    }

Unsafe類:

public final int getAndSetInt(Object var1, long var2, int var4) {
        int var5;
        do {
            var5 = this.getIntVolatile(var1, var2);
        } while(!this.compareAndSwapInt(var1, var2, var5, var4));

        return var5;
    }
public final native boolean compareAndSwapInt(Object var1,
         long var2, int var4, int var5);

經過上述三段源碼,能夠清楚看出,AtomicInteger中getAndSet這一原子方法是經過Unsafe中的原生方法compareAndSwapInt方法完成CAS機制,從而確保操做的原子性。

CAS還涉及到Java中鎖的實現,這個也留到鎖專題再細說,畢竟此次的主題是CAS,ABA及解決之道。

Why need CAS

那麼爲何須要CAS呢,畢竟Java已經有了多種手段來保證線程安全的原子性問題,最廣爲人知的除了Atomic包(底層是CAS),就是synchronized鎖了。

緣由很簡單,由於synchronized鎖什麼的過重了。這裏所說的重,是指其消耗的系統資源較多(因此又稱爲重量級鎖)。因此有着底層硬件支持的CAS纔會那麼受歡迎。固然CAS也有着本身的問題,這個後面會談到。

CAS應用:

說得再多,不如來點實際代碼,看看具體效果。

如下代碼,包含四個類:一個主類,用於調用實現類,展現效果(註釋中有執行結果)。三個實現類,分別展現了沒有處理,使用Atomic包,使用CAS三種方式來多線陳增長全局計數器的效果。

AtomicityWithNoDeal

未作任何處理,經過100個子線程分別執行10000次計數器+1操做。

package tech.jarry.learning.netease;
    
    /**
     * @Description:
     * @Author: jarry
     */
    public class AtomicityWithNoDeal {
    
        private volatile int i = 0;
    
        private void add(){
            i++;
        }
    
        public void run() throws InterruptedException {
            for (int j = 0; j < 100; j++){
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        for (int m = 0; m< 10000; m++){
                            add();
                        }
                        System.out.println(Thread.currentThread().getName()+" has run finished !");
                    }
                }).start();
            }
    
            Thread.sleep(2000);
            System.out.println("i: "+i);
        }
    }

AtomicityWithAtomic

進行Atomic包處理,經過100個子線程分別執行10000次計數器+1操做。

package tech.jarry.learning.netease;
    
    import java.util.concurrent.atomic.AtomicInteger;
    
    /**
     * @Description:
     * @Author: jarry
     */
    public class AtomicityWithAtomic {
    
        private AtomicInteger atomicInteger = new AtomicInteger(0);
    
        private void add(){
            atomicInteger.incrementAndGet();
        }
    
        public void run() throws InterruptedException {
    
            for (int j = 0; j < 100; j++){
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        for (int m = 0; m< 10000; m++){
                            add();
                        }
                        System.out.println(Thread.currentThread().getName()+" has run finished !");
                    }
                }).start();
            }
    
            Thread.sleep(2000);
            System.out.println("atomicInteger: "+atomicInteger.get());
        }
    }

AtomicityWithCAS

進行手寫的CAS處理,經過100個子線程分別執行10000次計數器+1操做。

package tech.jarry.learning.netease;
    
    import sun.misc.Unsafe;
    
    import java.lang.reflect.Field;
    
    /**
     * @Description:
     * @Author: jarry
     */
    public class AtomicityWithCAS {
    
        // 創建全局計數器,用於觀察CAS原子性特色
        volatile int k = 0;
        // 定義Unsafe引用對象
        private static Unsafe unsafe = null;
        // 定義k的內存偏移量(能夠理解爲k在內存中地址,固然實際與C指針的內存地址是徹底不一樣的)
        private static long valueOffset;
    
        static {
            try {
                // 利用反射獲取Unsafe實例對象(正常途徑是沒法獲取的)
                Field field = Unsafe.class.getDeclaredField("theUnsafe");
                field.setAccessible(true);
                // 因爲unsafe是靜態對象,因此傳入null。想一想也對,畢竟不一樣的實例對象的非靜態對象固然是不一樣的,固然須要傳入實例對象做爲參數嘍。
                // 另外吐槽一句,我查看這段資料的時候,發現百度第一頁的各個博客,幾乎都是同樣的示例代碼。。。
                unsafe = (Unsafe)field.get(null);
    
                // 獲取當前對象中全局計數器k的內存地址偏移
                Field kField = AtomicityWithCAS.class.getDeclaredField("k");
                kField.setAccessible(true);
                valueOffset = unsafe.objectFieldOffset(kField);
    
            } catch (NoSuchFieldException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
    
        /**
         * 執行全局計數器k+1的方法
         */
        private void add(){
            // 當CAS執行失敗時,須要從新執行相關操做,直到執行成功。故CAS是一個自旋鎖。
            while(true) {
                // 獲取CAS操做所需的舊值
                int current = unsafe.getIntVolatile(this,valueOffset);
                // 進行CAS操做
                if (unsafe.compareAndSwapInt(this,valueOffset,current,current+1)){
                    // 執行成功,就跳出循環
                    break;
                }
            }
        }
    
        /**
         * 爲了體現效果,這裏開啓了100個線程循環執行add()操做
         * @throws InterruptedException
         */
        public void run() throws InterruptedException {
            for (int j = 0; j < 100; j++){
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        // 每一個線程執行10000次add()操做
                        for (int m = 0; m< 10000; m++){
                            add();
                        }
                        System.out.println(Thread.currentThread().getName()+" has run finished !");
                    }
                }).start();
            }
    
            // 當前線程休眠2s,確保全部子線程執行完畢
            Thread.sleep(2000);
            System.out.println("k: "+k);
        }
    }

Main主函數

主線程調用同一包下的AtomicityWithNoDeal,AtomicityWithAtomic,AtomicityWithCAS三個類,觀察運行效果。

package tech.jarry.learning.netease;
    
    public class Main {
    
        public static void main(String[] args) throws InterruptedException {
    
    //      (new AtomicityWithNoDeal()).run();
            /**
             * 運行結果:
             * Thread-1 has run finished !
             * 。。。。。。(略98個線程)
             * Thread-83 has run finished !
             * i: 440239
             */
    
    //      (new AtomicityWithAtomic()).run();
            /**
             * 運行結果:
             * Thread-0 has run finished !
             * 。。。。。。(略98個線程)
             * Thread-69 has run finished !
             * atomicInteger: 1000000
             */
    
    //      (new AtomicityWithCAS()).run();
            /**
             * 運行結果:
             * Thread-1 has run finished !
             * 。。。。。。(略98個線程)
             * Thread-80 has run finished !
             * k: 1000000
             */
    
        }
    }

註釋

其實上述代碼中,重要的地方,我都寫上了相關的註釋。若是還有什麼不清楚的地方,能夠@我。

CAS缺點

CAS固然是有缺點的,不然就沒Synchronized什麼事情了。

  1. 從概念及代碼示例中能夠看出,當CAS操做執行失敗時,會繼續進入下一個循環執行,直到CAS操做執行成功,這種行爲稱爲自旋。自旋的實現讓全部線程都處於高頻運行,爭搶CPU執行時間的狀態。若是操做長時間不成功,會帶來很大的CPU資源消耗(因此Java有鎖的粗化/升級)。
  2. CAS僅能針對單個變量進行操做,不能用於多個變量來實現原子操做。
  3. ABA問題。

正如,我以前所提到的,看待技術問題要找到其特性的最初來源。如第二點中CAS之因此不能支持多個變量的原子操做,是由於CAS操做的原子性來源於硬件的支撐,而硬件只支持單個變量的原子操做,故CAS只能針對單個變量的原子操做進行操做。而有些文章或代碼中提到經過CAS執行多個變量的原子操做,其實本質並非針對多個變量,而是針對這些變量的集合或者總的對象的Reference操做的。這有點抽象,舉個栗子。我將經過CAS操做轉變了某個數組的引用變量的指向,看起來我實現了整個數組內多個元素轉變的原子操做。但實際是我經過改變當前引用變量的指向實現的,CAS的原子操做針對的是這個指向Reference。具體代碼能夠參照Atomic包中的AtomicIntegerArray與AtomicReference等實現。

至於第一點,細究起來有很是多的內容,如鎖的粗化,自旋是否能夠優化等。其實CAS的自旋操做實際是確保了必定有CAS操做在執行,但這是經過犧牲CPU實現的。舉個栗子,爲了可以監聽硬件串口返回的消息,我經過while(true)來不斷獲取串口發送過來的數據,直到我得到了一個完整數據包。

話頭收回來,讓咱們談談第三點-ABA問題。

ABA概念

ABA問題,說白了就是鑽了CAS機制的空子。

爲了更好地說明這個問題,咱們設定兩個線程,同時對變量i進行操做。

正常場景:

初始i=0;

線程-1(打算對i進行CAS操做)

線程-1:獲取i的舊值-0;

線程-1:設定i的新值-2;

線程-1:對i進行CAS操做,舊值i=0符合實際內存中i現有的值,執行swap操做,i=2;

看似正常的場景:

線程-1(打算對i進行CAS操做)

線程-1:獲取i的舊值-0;

線程-2:對i進行了CAS操做,將i改成10;

線程-1:設定i的新值-2;

線程-2:對i進行了CAS操做,將i從新改成0;

線程-1:對i進行CAS操做,舊值i=0符合實際內存中i現有的值,執行swap操做,i=2;

上述的兩個場景中線程-1都完成了想要完成的CAS操做,區別就是其中線程2曾經進行過一些操做。

固然這裏確定有朋友要說,這對程序的結果沒有任何的影響。是的,在現有的例子中確實對程序的運行結果毫無影響。

這裏我舉出兩個大佬給出的很是經典的例子,分別是極簡與複雜的表明。

極簡:你從銀行取出一箱子錢,放在了車上。結果你一個轉頭,小偷將裝滿錢的箱子拿走,並在原來的位置放了一個看起來如出一轍,但裝滿廢紙的箱子。你並無發現這一切,拿着這個箱子開開心心地回家了。囧。

複雜:經過單向鏈表展示ABA的潛在威脅。因爲例子比較複雜,我就不在這裏贅述。感興趣的朋友,能夠看看。

其實這兩個例子本質都是同樣的,想表達的就是咱們CAS操做的不是簡簡單單的數值,更有着其背後的深層信息(而後經過內存,鏈表,引用來證實觀點)。

這裏我要開始表達個人觀點了:現有的大部分博客或者文章都解釋得或多或少有必定問題。。只有部分大佬的博客提到了核心,可是爲了說了核心,又舉了不少例子(ABA問題的例子原本就很差舉,例子大多容易被誤解,後面會談到),致使核心論點被忘卻。而後又有不少人去借鑑,或者直接拿來這些例子,可是又不能很好地經過這些例子說明ABA,而後就經過本身的理解解釋了一番(更好理解,可是卻開始歪了),不斷有人進入這個圈子,而後解釋愈來愈歪。形成不少剛瞭解ABA的小白一臉茫然,看着那套看似正確的解釋,就入坑(雖然這個坑影響不那麼大,起碼對於絕大部分人員都沒太大影響。就像不少Java開發者不懂JMM,工做作得不也還能夠嘛)了。。。
(這裏插句題外話,那就是有關博客抄襲複製的問題。其實我不反對技術的之間的借鑑,畢竟重複造輪子是不可取的,只有有效的思想碰撞才能夠產生推進力嘛。可是經過爬蟲無腦爬取,或者直接複製粘貼全文,就真的有些過度了。之因此有這種感慨,是由於我如今有時候在百度查詢一些資料,十多篇博客看下來,竟然大部分都是同樣的,太影響效率了)

固然,說話要講道理的,不能只作「批評家」。就上述兩個經典例子存在一個很大的問題,那就是即便脫離了CAS,上述兩個例子中存在的問題,仍是存在。另一點佐證就是不少時候,咱們須要解決的就是簡單的數值的CAS問題,這個數值不牽涉什麼複雜的依賴關係。關於這點佐證的最有力說明就是輕量級鎖的CAS爲何不須要考慮ABA問題(由於其根本就不涉及什麼複雜依賴)。

話說回來,其實上述兩個例子,以及個人兩點說明,其實更傾向於表現ABA,距離ABA問題的本質,還差了那麼一句畫龍點睛的總結。

總結:ABA問題的本質就是因爲對多線程下CAS流程控制的缺少,而致使的信息缺失。表現出來的就是因爲缺少必要信息(小偷對箱子進行了操做),而產生了隱患

若是你仍是有些沒法理解這個結論,那你還記得程序的一個重要原則-程序置於控制下。若是你都沒法控制你的程序的行爲,那麼無疑,你的程序是有問題的。

ABA示例:

接下來經過銀行非法洗錢的例子,來簡單闡述由信息缺失,形成的問題。

ABATest

這是一個產生了ABA問題的示例。示例中銀行沒法發現客戶帳戶上的非法洗錢行爲。

package tech.jarry.learning.netease.casWithABA;
    
    import sun.misc.Unsafe;
    import tech.jarry.learning.netease.test.CounterUnsafe;
    
    import java.lang.reflect.Field;
    
    /**
     * @Description:
     * @Author: jarry
     */
    public class ABATest {
    
        volatile int k = 10;
        private static Unsafe unsafe = null;
        private static long valueOffset;
    
        static {
            try {
                Field field = Unsafe.class.getDeclaredField("theUnsafe");
                field.setAccessible(true);
                unsafe = (Unsafe)field.get(null);
    
                Field iField = CounterUnsafe.class.getDeclaredField("i");
                iField.setAccessible(true);
                valueOffset = unsafe.objectFieldOffset(iField);
            } catch (NoSuchFieldException e) {
                e.printStackTrace();
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            }
        }
    
        private void transferOld() throws InterruptedException {
            System.out.println("開始轉帳(舊系統:存在ABA問題)");
            while(true) {
                int current = unsafe.getIntVolatile(this,valueOffset);
                System.out.println("因爲CPU搶佔問題,轉帳程序阻塞100ms(爲了將可能出現的ABA問題,變成確定出現)");
                Thread.sleep(100);
                if (unsafe.compareAndSwapInt(this,valueOffset,current,current+1)){
                    System.out.println("銀行轉帳"+1+"元,成功。餘額:"+k);
                    break;
                }
                System.err.println("警告:帳戶存在交易記錄之外的資金流動");
            }
        }
    
        private void cleanMoneySub() {
            while(true) {
                int current = unsafe.getIntVolatile(this,valueOffset);
                if (unsafe.compareAndSwapInt(this,valueOffset,current,current-2)){
                    break;
                }
            }
            System.out.println("非法組織洗錢,盜走2元,餘額:"+k);
        }
    
        private void cleanMoneyAdd(){
            while(true) {
                int current = unsafe.getIntVolatile(this,valueOffset);
                if (unsafe.compareAndSwapInt(this,valueOffset,current,current+2)){
                    break;
                }
            }
            System.out.println("非法組織洗錢,加入2元,餘額:"+k);
        }
    
        public void oldSystemTransfer() throws InterruptedException {
            ABATest abaTest = new ABATest();
            System.out.println("帳戶餘額:"+k);
    
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        abaTest.transferOld();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
    
            Thread.sleep(20);
            abaTest.cleanMoneyAdd();
            Thread.sleep(20);
            abaTest.cleanMoneySub();
    
            Thread.sleep(200);
            System.out.println("銀行卡原來餘額爲10,接收轉帳1元,故指望餘額爲11元。實際餘額:"+abaTest.k);
        }
    
        public static void main(String[] args) throws InterruptedException {
            ABATest abaTest = new ABATest();
            abaTest.oldSystemTransfer();
            /**
             * 運行結果:
             * 帳戶餘額:10
             * 開始轉帳(舊系統:存在ABA問題)
             * 因爲CPU搶佔問題,轉帳程序阻塞100ms(爲了將可能出現的ABA問題,變成確定出現)
             * 非法組織洗錢,加入2元,餘額:12
             * 非法組織洗錢,盜走2元,餘額:10
             * 銀行轉帳1元,成功。餘額:11
             * 銀行卡原來餘額爲10,接收轉帳1元,故指望餘額爲11元。實際餘額:11
             */
        }
    }

ABAResolveTest

這是一個修復了ABA問題的示例。示例中銀行正常發現客戶帳戶上的非法洗錢行爲。

package tech.jarry.learning.netease.casWithABA;
    
    import java.util.concurrent.atomic.AtomicStampedReference;
    
    /**
     * @Description:
     * @Author: jarry
     */
    public class ABAResolveTest {
    
        private AtomicStampedReference<Integer> kReference = new AtomicStampedReference<>(10,0);
    
        private void transferNew() throws InterruptedException {
            System.out.println("開始轉帳(新系統:解決了ABA問題)");
            while(true) {
                Integer currentReference = kReference.getReference();
                int stamp = kReference.getStamp();
                System.out.println("因爲CPU搶佔問題,轉帳程序阻塞100ms");
                Thread.sleep(100);
                if (kReference.compareAndSet(currentReference,currentReference+1,stamp,stamp+1)){
                    System.out.println("銀行轉帳"+1+"元,成功。餘額:"+kReference.getReference());
                    break;
                }
                System.err.println("警告:帳戶存在交易記錄之外的資金流動");
            }
        }
    
        private void cleanMoneySub(){
            int stamp = kReference.getStamp();
            kReference.set(kReference.getReference()+2,stamp+1);
            System.out.println("非法組織洗錢,盜走2元,餘額:"+kReference.getStamp());
        }
    
        private void cleanMoneyAdd(){
            int stamp = kReference.getStamp();
            kReference.set(kReference.getReference()-2,stamp+1);
            System.out.println("非法組織洗錢,加入2元,餘額:"+kReference.getStamp());
        }
    
        private void newSystemTransfer() throws InterruptedException {
            ABAResolveTest abaResolveTest = new ABAResolveTest();
            System.out.println("帳戶餘額:"+abaResolveTest.kReference.getReference());
    
            new Thread(new Runnable() {
                @Override
                public void run() {
                    try {
                        abaResolveTest.transferNew();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }).start();
    
            Thread.sleep(20);
            abaResolveTest.cleanMoneyAdd();
            Thread.sleep(20);
            abaResolveTest.cleanMoneySub();
    
            Thread.sleep(200);
            System.out.println("銀行卡原來餘額爲10,接收轉帳1元,故指望餘額爲11元。實際餘額:"+abaResolveTest.kReference.getReference());
        }
    
        public static void main(String[] args) throws InterruptedException {
            ABAResolveTest abaResolveTest = new ABAResolveTest();
            abaResolveTest.newSystemTransfer();
            /**
             * 運行結果:
             * 帳戶餘額:10
             * 開始轉帳(新系統:解決了ABA問題)
             * 因爲CPU搶佔問題,轉帳程序阻塞100ms
             * 非法組織洗錢,加入2元,餘額:1
             * 非法組織洗錢,盜走2元,餘額:2
             * 警告:帳戶存在交易記錄之外的資金流動
             * 因爲CPU搶佔問題,轉帳程序阻塞100ms
             * 銀行轉帳1元,成功。餘額:11
             * 銀行卡原來餘額爲10,接收轉帳1元,故指望餘額爲11元。實際餘額:11
             */
        }
    }

上述的兩個例子,也許不是最適合的,但確實闡述了我想要表達的想法。

話說回來,只有到本身寫demo時,才能理解大佬寫ABA的demo時心裏的掙扎啊。囧

ABA問題的解決

ABA問題的解決,說白了就是經過引入版本號,從而解決ABA問題的形成的隱患。

用個人話說呢,就是經過引入版本號,瞭解到線程執行操做時,是否有別的線程作了相似ABA的事情,從而使得本線程的CAS操做從新執行。這裏爲何從新執行,由於簡單啊。固然,也能夠如我那樣打個輸出或者註釋什麼的(可能會浪費系統資源)。無論怎麼處理,起碼此次我知道有這麼個問題了。囧。

小結

至此,CAS機制,ABA問題及解決方案,都已經敘述完畢了。

核心總結:

ABA問題的本質就是因爲對多線程下CAS流程控制的缺少,而致使的信息缺失。表現出來的就是因爲缺少必要信息,而產生了隱患

該說的差很少都說了,簡單回顧一下:

  • 凡事都有其利弊,每每弊端就是因爲其優勢帶來的。如CAS的硬件支持。
  • 技術的學習,須要追尋技術特性的真正來源,才能夠一步步走向架構師。
  • 學習,一方面須要尋求多方資料,另外一方面也須要本身的理解與驗證。
  • 遇到沒法理解或者沒法解讀的事物時,就去尋找它的定義,它的原則。
相關文章
相關標籤/搜索