volatile關鍵字在Android中到底有什麼用?

本文同步發表於個人微信公衆號,掃一掃文章底部的二維碼或在微信搜索 郭霖 便可關注,每一個工做日都有文章更新。

上週六在公衆號分享了一篇關於Java volatile關鍵字的文章,發佈以後有朋友在留言裏指出,說這個關鍵字沒啥用啊,Android開發又不像服務器那樣有那麼高的併發,老分享這種知識幹啥?java

讓我意識到有些朋友對於volatile這個關鍵字的理解仍是有誤區的。編程

另外也有朋友留言說,雖然知道volatile關鍵字的做用,可是想不出在Android開發中具體有什麼用途。緩存

因此我準備寫篇文章來剖析一下這個關鍵字,順便回答一下這些朋友的疑問。安全

因爲這篇文章是我用週日一天時間趕出來的,因此可能不會像平時的文章那樣充實,可是對於上述問題我相信仍是能夠解釋清楚的。服務器

對volatile關鍵字的做用有疑問的同窗,可能都不太瞭解CPU高速緩存這個概念,因此咱們先從這個概念講起。

微信

CPU高速緩存和可見性問題

當一個程序運行的時候,數據是保存在內存當中的,可是執行程序這個工做倒是由CPU完成的。那麼當CPU正在執行着任務呢,忽然須要用到某個數據,它就會從內存中去讀取這個數據,獲得了數據以後再繼續向下執行任務。網絡

這是理論上理想的工做方式,可是卻存在着一個問題。咱們知道,CPU的發展是遵循摩爾定律的,每18個月左右集成電路上晶體管的數量就能夠翻一倍,所以CPU的速度只會變得愈來愈快。多線程

可是光CPU快沒有用呀,由於CPU再快仍是要從內存去讀取數據,而這個過程是很是緩慢的,因此就大大限制了CPU的發展。併發

爲了解決這個問題,CPU廠商引入了高速緩存功能。內存裏存儲的數據,CPU高速緩存裏也能夠存一份,這樣當頻繁須要去訪問某個數據時就不須要重複從內存中去獲取了,CPU高速緩存裏有,那麼直接拿緩存中的數據便可,這樣就能夠大大提高CPU的工做效率。ide

而當程序要對某個數據進行修改時,也能夠先修改高速緩存中的數據,由於這樣會很是快,等運算結束以後,再將緩存中的數據寫回到內存當中便可。

這種工做方式在單線程的場景下是沒問題的,準確來說,在單核多線程的場景下也是沒問題的。但若是到了多核多線程的場景下,可能就會出現問題。

咱們都知道,如今不論是手機仍是電腦,動不動就聲稱是多核的,多核就是CPU中有多個運算單元的意思。由於一個運算單元在同一時間其實只能處理一個任務,即便咱們開了多個線程,對於單核CPU而言,它只能先處理這個線程中的一些任務,而後暫停下來轉去處理另一個線程中的任務,以此交替。而多核CPU的話,則能夠容許在同一時間處理多個任務,這樣效率固然就更高了。

可是多核CPU又帶來了一個新的挑戰,那就是在多線程的場景下,CPU高速緩存中的數據可能不許確了。緣由也很簡單,咱們經過下面這張圖來理解一下。

能夠看到,這裏有兩個線程,分別經過兩個CPU的運算單元來執行程序,但它們是共享同一個內存的。如今CPU1從內存中讀取數據A,並寫入高速緩存,CPU2也從內存中讀取數據A,並寫入高速緩存。

到目前爲止仍是沒有問題的,可是若是線程2修改了數據A的值,首先CPU2會更新高速緩存中A的值,而後再將它寫回到內存當中。這個時候,線程1再訪問數據A,CPU1發現高速緩存當中有A的值啊,那麼直接返回緩存中的值不就好了。此時你會發現,線程1和線程2訪問同一個數據A,獲得的值卻不同了。

這就是多核多線程場景下遇到的可見性問題,由於當一個線程去修改某個變量的值時,該變量對於另一個線程並非當即可見的。

爲了讓以上理論知識更具備說服力,這裏我編寫了一個小Demo來驗證上述說法,代碼以下所示:

public class Main {

    static boolean flag;

    public static void main(String... args) {
        new Thread1().start();
        new Thread2().start();
    }

    static class Thread1 extends Thread {
        @Override
        public void run() {
            while (true) {
                if (flag) {
                    flag = false;
                    System.out.println("Thread1 set flag to false");
                }
            }
        }
    }

    static class Thread2 extends Thread {
        @Override
        public void run() {
            while (true) {
                if (!flag) {
                    flag = true;
                    System.out.println("Thread2 set flag to true");
                }
            }
        }
    }

}

這段代碼真的很是簡單,咱們開啓了兩個線程來對同一個變量flag進行修改。Thread1使用一個while(true)循環,發現flag是true時就把它改成false。Thread2也使用一個while(true)循環,發現flag是false時就把它改成true。

理論上來講,這兩個線程同時運行,那麼就應該一直交替打印,你改個人值,我再給你改回去。

實際上真的會是這樣嗎?咱們來運行一下就知道了。

能夠看到,打印過程只持續了一小會就中止打印了,可是程序卻沒有結束,依然顯示在運行中。

這怎麼可能呢?理論上來講,flag要麼爲true,要麼爲false。true的時候Thread1應該打印,false的時候Thread2應該打印,兩邊都不打印是爲何呢?

咱們用剛纔所學的知識就能夠解釋這個本來解釋不了的問題,由於Thread1和Thread2的CPU高速緩存中各有一份flag值,其中Thread1中緩存的flag值是false,Thread2中緩存的flag值是true,因此兩邊就都不會打印了。

這樣咱們就經過一個實際的例子演示了剛纔所說的可見性問題。那麼該如何解決呢?

答案很明顯,volatile。

volatile這個關鍵字的其中一個重要做用就是解決可見性問題,即保證當一個線程修改了某個變量以後,該變量對於另一個線程是當即可見的。

至於volatile的工做原理,太底層方面的內容我也說不上來,大概原理就是當一個變量被聲明成volatile以後,任何一個線程對它進行修改,都會讓全部其餘CPU高速緩存中的值過時,這樣其餘線程就必須去內存中從新獲取最新的值,也就解決了可見性的問題。

咱們能夠將剛纔的代碼進行以下修改:

public class Main {

    volatile static boolean flag;
    ...

}

沒錯,就是這麼簡單,在flag變量的前面加上volatile關鍵字便可。而後從新運行程序,效果以下圖所示。

一切如咱們所預期的那樣運行了。

指令重排問題

volatile關鍵字還有另一個重要的做用,就是禁止指令重排,這又是一個很是有趣的問題。

咱們先來看兩段代碼:

// 第一段代碼
int a = 10;
int b = 5;
a = 20;
System.out.println(a + b);

// 第二段代碼
int a = 10;
a = 20;
int b = 5;
System.out.println(a + b);

第一段代碼,咱們聲明瞭一個a變量等於10,又聲明瞭一個b變量等於5,而後將a變量的值改爲了20,最後打印a + b的值。

第二段代碼,咱們聲明瞭一個a變量等於10,而後將a變量的值改爲了20,又聲明瞭一個b變量等於5,最後打印a + b的值。

這兩段代碼有區別嗎?

不用瞎猜了,這兩段代碼沒有任何區別,聲明變量b和修改變量a之間的順序是隨意的,它們之間誰也不礙着誰。

也正是由於這個緣由,CPU在執行代碼時,其實並不必定會嚴格按照咱們編寫的順序去執行,而是可能會考慮一些效率方面的緣由,對那些前後順序可有可無的代碼進行從新排序,這個操做就被稱爲指令重排。

這麼看來,指令重排這個操做沒毛病啊。確實,但只限在單線程環境下。

不少問題一旦進入了多線程環境,就會變得更加複雜,咱們來看以下代碼:

public class Main {

    static boolean init;
    static String value;

    static class Thread1 extends Thread {
        @Override
        public void run() {
            value = "hello world";
            init = true;
        }
    }

    static class Thread2 extends Thread {
        @Override
        public void run() {
            while (!init) {
                // 等待初始化完成
            }
            value.toUpperCase();
        }
    }

}

這段代碼的思路仍然很簡單,Thread1用於對value數據進行初始化,初始化完成以後會將init設置成true。Thread2則會先經過while循環等待初始化完成,完成以後再對value數據進行操做。

那麼這段代碼能夠正常工做嗎?未必,由於根據剛纔的指令重排理論,Thread1中value和init這兩個變量之間是沒有前後順序的。若是CPU將這兩條指令進行了重排,那麼就可能出現初始化已完成,可是value尚未賦值的狀況。這樣Thread2的while循環就會跳出,而後在操做value的時候出現空指針異常。

因此說,指令重排功能一旦進入了多線程環境,也是可能會出現問題的。

而至於解決方案嘛,固然仍是volatile了。

對某個變量聲明瞭volatile關鍵字以後,同時也就意味着禁止對該變量進行指令重排。因此咱們只須要這樣修改代碼就可以保證程序的安全性了。

public class Main {

    volatile static boolean init;
    ...

}

volatile在Android上的應用

如今咱們已經瞭解了volatile關鍵字的主要做用,可是就像開篇時那位朋友提到的同樣,不少人想不出來這個關鍵字在Android上有什麼用途。

其實我以爲任何一個技術點都不該該去生搬硬套,你只要掌握了它,該用到時能想到它就能夠了,而不是絞盡腦汁去想我到底要在哪裏使用它。

我在看一些Google庫的源碼時,其實時不時就能看到這個關鍵字,只要是涉及多線程編程的時候,volatile的出場率仍是不低的。

這裏我給你們舉一個常見的示例吧,在Android上咱們應該都編寫過文件下載這個功能。在執行下載任務時,咱們須要開啓一個線程,而後從網絡上讀取流數據,並寫入到本地,重複執行這個過程,直到全部數據都讀取完畢。

那麼這個過程我能夠用以下簡易代碼進行表示:

public class DownloadTask {

    public void download() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (true) {
                    byte[] bytes = readBytesFromNetwork(); // 從網絡上讀取數據
                    if (bytes.length == 0) {
                        break; // 下載完畢,跳出循環
                    }
                    writeBytesToDisk(bytes); // 將數據寫入到本地
                }
            }
        }).start();
    }

}

到此爲止沒什麼問題。

不過如今又來了一個新的需求,要求容許用戶取消下載。咱們都知道,Java的線程是不能夠中斷的,因此若是想要作取消下載的功能,通常都是經過標記位來實現的,代碼以下所示:

public class DownloadTask {

    boolean isCanceled = false;

    public void download() {
        new Thread(new Runnable() {
            @Override
            public void run() {
                while (!isCanceled) {
                    byte[] bytes = readBytesFromNetwork();
                    if (bytes.length == 0) {
                        break;
                    }
                    writeBytesToDisk(bytes);
                }
            }
        }).start();
    }

    public void cancel() {
        isCanceled = true;
    }

}

這裏咱們增長了一個isCanceled變量和一個cancel()方法,調用cancel()方法時將isCanceled變量設置爲true,表示下載已取消。

而後在download()方法當中,若是發現isCanceled變量爲true,就跳出循環再也不繼續執行下載任務,這樣也就實現了取消下載的功能。

這種寫法可以正常工做嗎?根據個人實際測試,確實基本上都是能夠正常工做的。

可是這種寫法真的安全嗎?不,由於你會發現download()方法和cancel()方法是運行在兩個線程當中的,所以cancel()方法對於isCanceled變量的修改,未必對download()方法就當即可見。

因此,存在着這樣一種可能,就是咱們明明已經將isCanceled變量設置成了true,可是download()方法所使用的CPU高速緩存中記錄的isCanceled變量仍是false,從而致使下載沒法被取消的狀況出現。

所以,最安全的寫法就是對isCanceled變量聲明volatile關鍵字:

public class DownloadTask {

    volatile boolean isCanceled = false;
    ...

}

這樣就能夠保證你的取消下載功能始終是安全的了。

好了,關於volatile關鍵字的做用,以及它在Android開發中具體有哪些用途,相信到這裏就解釋的差很少了。

原本是想用週日一天時間寫篇小短文的,寫着寫着好像最後又寫出了很多內容,不過只要對你們有幫助就好。


若是想要學習Kotlin和最新的Android知識,能夠參考個人新書 《第一行代碼 第3版》點擊此處查看詳情


關注個人技術公衆號,每一個工做日都有優質技術文章推送。

微信掃一掃下方二維碼便可關注:

相關文章
相關標籤/搜索