由java "double check"說起

引子


在java中,為了保證某種資源只被初始化一次,我們通常會將其放入同步代碼塊中,如:

public synchronized Resource getResource(){  
  if (resource == null){   
        resource = new Resource();    
  }  
  return resource;  
}  

上面的代碼可以保證正確性,但是執行效率上似乎還有優化的空間:無論resource是否已經被初始化,都會對getResource方法加鎖。聰明如你一定很快就會想到使用"double check"的方式提高效率:

private Resource resource;

public Resource getResource() {
  Resource tmp = this.resource;  
  if (tmp == null) {   
    synchronized(this){
      tmp = this.resource   
      if (tmp == null) {  
        this.resource = tmp = new Resource();    
      }     
    }    
  }  
  return tmp;  
}  

ok,看起來很完美,然而,上面的代碼是有問題的。不過,在jdk 5 以后,只要給成員變量resource加上volatile就可以解決這個問題了。

private volatile Resource resource;

public Resource getResource() {
  Resource tmp = this.resource;  
  if (tmp == null) {   
    synchronized(this){
      tmp = this.resource   
      if (tmp == null) {  
        this.resource = tmp = new Resource();    
      }     
    }    
  }  
  return tmp;  
}  

那么,volatile有什么作用,第一個版本的代碼問題在哪里,為何加上volatile后就可以正常使用?下面我們就從java的內存模型開始,分析這些問題。

volatile的作用


volatile有兩大作用:

  1. 保證內存可見性

  2. 防止指令重排

一、內存可見性

java的內存模型規定:在多線程情況下,線程操作主內存變量,需要通過線程獨有的工作內存拷貝主內存變量副本來進行。此處的所謂內存模型要區別于通常所說的虛擬機堆模型:

多線程下內存操作

Java內存模型也規定了工作內存與主內存之間交互的協議,定義了8種原子操作:

(1) lock:將主內存中的變量鎖定,為一個線程所獨占

(2) unclock:將lock加的鎖定解除,此時其它的線程可以有機會訪問此變量

(3) read:將主內存中的變量值讀到工作內存當中

(4) load:將read讀取的值保存到工作內存中的變量副本中。

(5) use:將值傳遞給線程的代碼執行引擎

(6) assign:將執行引擎處理返回的值重新賦值給變量副本

(7) store:將變量副本的值存儲到主內存中。

(8) write:將store存儲的值寫入到主內存的共享變量當中。

從上面的描述可以看出,當一個線程修改了某個共享變量的值并同步到主線程時,在其它線程從主內存將變量同步回自己的工作內存之前,共享變量的改變對其是不可見的。所以其他線程的本地內存中的變量已經是過時的,并不是更新后的值。

為了解決上面的問題,可以使用volatile關鍵字:

線程中每次use變量時,都需要連續執行read->load->use幾項操作,即所謂的每次使用都要從主內存更新變量值,這樣其它線程的修改對該線程就是可見的。
線程每次assign變量時,都需要連續執行assign->store->write幾項操作,即所謂每次更新完后都會回寫到主內存,這樣使得其它線程讀到的都是最新數據。

二、指令重排

什么是指令重排?指令重排是指JVM為了優化指令,提高程序運行效率,在不影響單線程程序執行結果的前提下,盡可能地提高并行度。編譯器、處理器也遵循這樣一個目標。在單線程的情況下,指令重排不會對程序執行結果造成影響,然而在多線程的情況下,指令重排就會出現問題,看下面的代碼:

public class PossibleReordering {
static int x = 0, y = 0;
static int a = 0, b = 0;

public static void main(String[] args) throws InterruptedException {
    Thread a = new Thread(new Runnable() {
        public void run() {
            a = 1;
            x = b;
        }
    });

    Thread b = new Thread(new Runnable() {
        public void run() {
            b = 1;
            y = a;
        }
    });
    a.start();
    b.start();
    a.join();
    b.join();
    System.out.println(“(” + x + “,” + y + “)”);
}

上面這段代碼的結果是什么?考慮線程的執行順序,可能為 (0,1) 、(1,0)或者是(1,1)。但是如果運行上面的程序若干次,還有可能出現第四種情況(0,0),這就是由于指令重排改變了執行順序導致的。

看下面的語句:

double r = 2.3d;
double pi =3.1415926; 
double area =  r * r * pi; 

這個語句有如下步驟:

  1. 將2.3賦值給r
  2. 將π賦值給pi
  3. 讀取r的值
  4. 讀取pi的值
  5. 計算area的值

jvm為了保證語義的正確性,操作1可能會和2、4交換次序,操作2可能會和1、3交換次序,但是1、3、5和2、4、5之間的執行順序肯定不會改變,這樣就保證了執行結果的一致性。但是,如上面的例子所示,在多線程情況下亂序執行可能會導致一些出乎意料之外的結果。

那么如果防止指令重排呢?可以使用內存屏障。內存屏障是一種CPU指令,用于控制特定條件下的重排序和內存可見性問題。Java編譯器也會根據內存屏障的規則禁止重排序。

內存屏障可以被分為以下幾種類型

  • LoadLoad屏障:對于這樣的語句Load1; LoadLoad; Load2,在Load2及后續讀取操作要讀取的數據被訪問前,保證 Load1要讀取的數據被讀取完畢。
  • StoreStore屏障:對于這樣的語句Store1; StoreStore; Store2,在Store2及后續寫入操作執行前,保證Store1的寫入操作對其它處理器可見。
  • LoadStore屏障:對于這樣的語句Load1; LoadStore; Store2,在Store2及后續寫入操作被刷出前,保證Load1要讀取的數據被讀取完畢。
  • StoreLoad屏障:對于這樣的語句Store1; StoreLoad; Load2,在Load2及后續所有讀取操作執行前,保證Store1的寫入對所有處理器可見。

在使用了volatile后:

  • 在每個volatile寫操作的前面插入一個StoreStore屏障。

  • 在每個volatile寫操作的后面插入一個StoreLoad屏障。

  • 在每個volatile讀操作的后面插入一個LoadLoad屏障。

  • 在每個volatile讀操作的后面插入一個LoadStore屏障。

這樣就可以防止指令重排導致的問題。

三、版本一 "double check" 的問題

有了上面的知識,我們就可以來分析第一個 "double check" 的問題了:

private Resource resource;

public Resource getResource() {
  Resource tmp = this.resource;  
  if (tmp == null) {   
    synchronized(this){
      tmp = this.resource   
      if (tmp == null) {  
        this.resource = tmp = new Resource();    
      }     
    }    
  }  
  return tmp;  
}  

new Resource() 可以分解為:

 memory =allocate();     //1:分配對象的內存空間 
 ctorInstance(memory);   //2:初始化對象 
 instance =memory;       //3:設置instance指向剛分配的內存地址

如果被重排為

 memory = allocate();     //1:分配對象的內存空間 
 instance = memory;       //2:設置instance指向剛分配的內存地址
 ctorInstance(memory);   //3:初始化對象 

就會出現線程A中執行這段賦值語句,在完成對象初始化之前就已經將其賦值給resource引用,恰好另一個線程進入方法判斷instance引用不為null,然后就將其返回使用,導致出錯。將resource設置為volatile之后,可以保證對相關操作的順序。

另外,對于final,也有其獨特的作用,例如在如下語句中,構建方法邊界前后的指令都不能重排序:

x.finalField = v;
 ... ;
構建方法邊界
sharedRef = x;
v.afield = 1;
x.finalField = v;
... ;
構建方法邊界
sharedRef = x;

總結

volatile在jdk 5 以后有雙重語義:1. 保證內存可見性 2.防止語句重排。不過這樣也會使編譯器放棄對相關代碼的優化,因此,如無必要,不要使用volatile修飾成員變量。

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 227,797評論 6 531
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,179評論 3 414
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事。” “怎么了?”我有些...
    開封第一講書人閱讀 175,628評論 0 373
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,642評論 1 309
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,444評論 6 405
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 54,948評論 1 321
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,040評論 3 440
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,185評論 0 287
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,717評論 1 333
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,602評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,794評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,316評論 5 358
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,045評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,418評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,671評論 1 281
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,414評論 3 390
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,750評論 2 370

推薦閱讀更多精彩內容