Java 之 關於volatile的一些理解

1.volatile

volatile關鍵字是一個特徵修飾符,確保本條指令不會因編譯器的優化而省略。可以li理解爲阻止編譯器對代碼進行優化。

先了解一下原子性(atomicity)和 可見性(visibility)以及有序性

1.1原子性

即一個操作或者一段代碼,要麼全部執行並且執行過程中不被任何因素打算,要麼不執行。

1.2原子操作

1.2.1處理器實現原子操作-(總線鎖、緩存鎖)

1.處理器會自動保證內存操作的原子性:即從內存中讀取或寫入一個字節的操作是原子的。處理器處理一個字節的時候,其它處理器不能訪問這個字節的內存地址。

2.總線鎖保證原子性:即處理器會提供一個LOCK信號,當一個處理器在總線上輸出此信號時,會阻塞其它處理器的請求。該處理器獨佔共享內存。

3.緩存鎖保證原子性:因爲鎖住總線的消耗太大,於是有了緩存鎖。即在LOCK期間,處理器緩存的內存區域將會被鎖定。其它處理器無法處理該塊內存中的數據。

1.2.2Java實現原子操作

Java實現原子操作的方式是鎖(悲觀鎖,樂觀鎖)和循環CAS

悲觀鎖:當發生衝突時,即多個線程去爭搶共享資源的時候。只有一個線程會獲得資源。即獲得鎖,也稱獨佔鎖或互斥鎖。其它線程將會被阻塞。直到鎖被釋放。例如Java中使用synchronized關鍵字。

樂觀鎖:CAS就是樂觀鎖的一種實現方式。當一個線程嘗試去獲取鎖,若是發現鎖被佔用,會繼續嘗試獲取鎖,直到獲取成功。

CAS:compare and swap CAS的思想可以這樣理解。有三個值,當前內存值A,舊的預期值B,更新值C。當且僅當A=C時,修改A爲C,並返回true。否則什麼都不做,返回false。即確認更改再賦值

1.3可見性

可見性是指當多個線程操作同一變量時,一個線程修改了變量的值,其它變量能夠立刻看到被修改的值。

1.4有序性

即程序執行的順序按照代碼編寫的順序來執行。

int i = 1;
int b = 2;
boolean y = false;

// 語句1
i = 3;

// 語句2
b = 4;

爲什麼會有有序性的概念呢?

如以上代碼,從代碼順序上來看,語句1是排在語句2前面的。但是JVM在執行的時候卻不一定能保證語句1在語句2之前執行。這是因爲編譯器會對代碼進行優化,它不保證程序中的各個語句的執行順序同代碼中的相同。但它會保證執行的最終結果是一致的。

是什麼會造成代碼執行順序的變化呢?答案是指令重排序。處理器會通過指令重排序來提高程序的運行效率。

指令重排序並不會影響單個線程內代碼執行的結果。

boolean y = false;
int i = 1;
// 線程一
i = 4;
y = true;

//  線程2
if (y) {
  i = 5;
}

在線程併發執行的時候,線程2有可能在xian線程1執行y=true之前已經完成執行。從而導致程序的執行沒有達到我們的預期。

總結:要想併發程序正確地執行,必須要同時保證原子性、可見性和有序性。只要其中一個沒有保證,就有可能保證程序運行不正確。

2.volatile關鍵字的作用

通過了解關於併發編程的原子性、可見性以及有序性之後。就是禁止指令重排序。使代碼按照我們的期望來執行。但是volatile的作用也是有限的。

當多個線程執行同一個方法對一個變量進行自增操作的時候。實際數據結果總是會小於 線程數*執行次數。

這是因爲自增操作不是原子操作,而volatile只能保證可見性,並不能保證原子性。

總結:volatile關鍵字能夠用來幫助我們實現線程安全,但是應用十分有限。即:變量的當前值和修改值之間沒有約束。

3.使用場景

3.1狀態標識

用代碼來說明問題

volatile boolean tag = true;

public void stop(){
   tag = false;
}

public void start() {
   while(tag) {
     // TO DO
   }
}

線程1執行start()的時候,我們可以使用其它的線程來調用stop()。由於使用了共同變量,使用volatile關鍵字保證了其它線程對tag的修改對於線程1可見。能夠使線程1及時收到終止信號。

3.2你有可能獲得不完整的對象

    /* volatile 關鍵字,解決指令重排序的問題 **/
    private volatile static LazyDoubleCheckSingleton lazy = null;

    private LazyDoubleCheckSingleton () {}

    public static LazyDoubleCheckSingleton getInstance() {
        if (lazy == null) {   //  這裏的if是一個避免鎖競爭的優化
            synchronized (LazyDoubleCheckSingleton.class) {
                //  這裏的if判斷防止多個進入外層if條件後實例被覆蓋
                if (lazy == null) {
                    lazy = new LazyDoubleCheckSingleton();
                    /* CPU 執行的時候會轉換成JVM指令來執行 **/
                    /*    指令重排序 (volatile)
                     *    CPU 執行時第二步和第三步有可能會顛倒
                     */
                    /* 1、分配內存給目標對象(當一個對象 new ,被分配內存) **/
                    /* 2、初始化對象  此時 lazy == null  **/   
                    /* 3、將初始化好的對象和內存地址建立關聯(給這個對象賦值) lazy != null 但未完成初始化 **/
                    /* 4、用戶初次訪問  **/
                }
            }
        }
        return lazy;
    }

通過經典的雙重檢查鎖定示例可以發現,變量lazy在未經volatile關鍵字修飾的情況下。

當處理器進行指令重排序,即 執行指令順序變成1》3》2》4,指令3執行完成,指令2未執行的時候。有其他線程來調用單例,將會返回一個不完整的對象。

總結:通過靈活運用volatile和synchronized關鍵字可以幫助我們寫出高校的代碼,比如可以使用synchronized控制變量的寫操作,使用volatile來控制變量的讀操作,從而提高性能。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章