android 多線程 — volatile

ps: 老文章拆分出來的內容

并發三原則

只有保證了這三點才能在并發中獲得想要的結果,我們想要進一步了解多線程,這3個原則必須要明白,并發都是圍繞這2個原則展開的

簡單來說是這樣的

可見性 - 是指線程之間的可見性,一個線程修改的狀態對另一個線程是可見的。也就是一個線程修改的結果,另一個線程馬上就能看到。簡單來說,線程的私有內存中對象副本和主內存對象的數據之間就是可見性問題,線程不把他私有內存中的對象副本協會到主內存中,那么對于其他想操作這個對象的線程來說就是"不可見的" ,最新數據不可見,所以此時其他線程可以獲取該對象的舊數據

原子性 - 對基本類型變量的讀取和賦值操作是原子性操作,即這些操作是不可中斷的,要么執行完畢,要么就不執行。在多線程中,原子性是線程不安全的,其實和可見性有關聯,線程在計算數據時會把在自己的工作區域 copy 一份數據的副本,然后計算的是這個數據的副本,最后再把數據學會內存中,這個過程里要是有別人線程同時操作同一個數據,那么我們的計算結果就是不正確了,就像打電話串線一樣,結果肯定不對

x =3;    
y =4;    
z = x+y;
x++;  

上面第三行就包括了多個操作,1是先讀取x的值,2讀取y的值,3將計算中的值,4把z的值寫回內存。一般一個語句含有多個操作該語句就不是原子性的操作,只有最簡單的讀取和賦值才是原子性的操作

有序性 - 即程序執行的順序按照代碼的先后順序執行,上面講了因為原子性問題,絕部分操作都是可以再分的,分成多個操作,這其中有對數據的讀,改,寫等,這些操作速度不一,JVM 為了提高效率,在保證結果相同的前提下,有計劃的多這么操作分組,在執行需要等待的操作中,穿插執行其他執行速度塊的操作,這叫指令重排序

指令重排序指的是在 保證程序最終執行結果和代碼順序執行的結果一致的前提下,改變語句執行的順序來優化輸入代碼,提高程序運行效率。

重排序在單線程中沒啥問題,咱們等著執行結果唄,反正重排序保證結果正確。但是在多先撤我那個環境下是存在并發的,你這里對某一個對象的執行重排序了,但是不是瞬間完成的,這時另外的線程可以操作這個對象,那么你這個重排序后的執行可能造成此時對象數據的不正確,會對其他線程使用這個對象產生影響

以下面的舉個例子:

int i = 0;              
boolean flag = false;
i = 1; //語句1           
flag = true; //語句2

定義了一個整形和Boolean型變量,并通過語句1和語句2對這兩個變量賦值,但是JVM在執行這段代碼的時候并不保證語句1在語句2之前執行,也就是說可能會發生 指令重排序。

再來個例子:

//線程1:
context = loadContext(); //語句1
inited = true; //語句2
 
//線程2:
while (!inited) {
    sleep()
}

doSomethingWithConfig(context);

對于線程1來說,語句1和語句2沒有依賴關系,因此有可能會發生指令重排序的情況。但是對于線程2來說,語句2在語句1之前執行,那么就會導致進入doSomethingWithConfig函數的時候context沒有初始化

Java 提供了3個關鍵字 volatile、synchronized 和 final 來實現并發3原則

  • final - 最好理解,一切都是不可變的,所以不在乎有多少個線程同時操作這個資源
  • synchronized - 之前的文章介紹了,synchronized 保證了有序性,你想 synchronized 使用一把鎖鎖住了資源,那別人想用只能等著,即便你再怎么重排序,我也能保證執行效果
  • volatile - 就比較復雜了,也是本文的重點

volatile

Volatile 是面試官最愛文的,即便你是做 android 開發的,你也逃不出去,所以大家好好鉆研吧

volatile 的特性: 先是非同步的 -> 保證了可見性 -> 同時也保證有序性 -> 但是不保證原子性

  • 非同步 - volatile 修飾的變量不是 synchronized 的,不是同步的,同一時間是能被多個線程操作的,所以 volatile 的使用范圍比較窄,多用于修飾 static 靜態變量

  • 保證可見性 - 好多地方都說 volatile 修飾的變量,線程直接和內存交互,不會保存副本,而實際上線程還是會保存副本,只不過 CPU 每次都會從內存中拿到最新的值,并且改變數據之后立馬寫回內存并通知其他改數據的備份數據改變了,看上去就像線程直接和內存交互一樣

  • 不保證原子性 - volatile 語義并不能保證變量的原子性。對任意單個volatile變量的讀/寫具有原子性,但類似于i++、i–這種復合操作不具有原子性,因為自增運算包括讀取i的值、i值增加1、重新賦值3步操作,并不具備原子性

  • 保證有序性 - volatile 能夠屏蔽指令重排序:

    • 當程序執行到volatile變量的讀操作或者寫操作時,在其前面的操作的更改肯定全部已經進行,且結果已經對后面的操作可見;在其后面的操作肯定還沒有進行;
    • 在進行指令優化時,不能將在對volatile變量訪問的語句放在其后面執行,也不能把volatile變量后面的語句放到其前面執行。

volatile 適用場景:

  • 禁止系統重排序的情況
  • 只有一個線程寫,多個線程讀的情況
  • 關鍵的標記行參數
  • 靜態單例

當然了 volatile 最經典的用處就是單例了

public class RxBus {

    public static volatile RxBus instance;

    public static RxBus getInstance() {
        if (instance == null) {
            synchronized (RxBus.class) {
                if (instance == null) {
                    instance = new RxBus();
                }
            }
        }
        return instance;
    }
}

我們對于靜態單例使用了 volatile 就能保證整個方法的執行順序是按照我們縮寫的執行。

若是我們不加 volatile ,在多線程時指令重排序,一個線程發現 instance 是 null 的就會 new 一個對象出來,此時因為指令沖排序,很可能先在內存 new 一塊空間然后賦值給 instance ,然后再去執行實例化對象的操作,對象實例化的操作是比較重的。這是領一額個線程進來,發現 instance 不是 null ,然后就去執行代碼,但是此時 instance 實際只是有了一塊內存地址,但是對象本身還沒初始化,就會產生空指針的問題

volatile 的優勢是同步性能開銷比鎖低很多,若是使用 synchronized + 鎖,切換鎖給不同的線程要好幾毫秒,比 new 個線程對象都耗費時間多了

但是 volatile 也有很嚴重的問題,那就是 volatile 不能保證原子性,雖然 volatile 讓內存可以同步到所有地方,但是并不能阻止多個線程同時操作同一個數據,是沒法保證原子性的,所以是不能代替 synchronized + 鎖,因此我們在使用 volatile 要及其小心,要思考會不會帶來并發問題,一般我們見到的 volatile 應用都很少,也都很死,都是固定的幾個場景使用


參考文檔:

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容