Java內存模型和Volatile關鍵字

前言

學習併發關鍵在於學會解決併發過程中線程通信以及線程同步中出現的問題,線程通信有兩類機制,一是共享內存,另一個是消息傳遞。JAVA使用的是第一種,通過在共享內存中進行讀寫來進行消息傳遞,在共享內存中,線程通信是隱性的,對編程人員是透明的,因此容易出現可見性問題;線程同步則是顯性的,需要編程人員來指定線程之間的互斥以及同步。高效併發系列皆是圍繞着介紹虛擬機如何實現線程、多線程之間由於共享和競爭數據而導致的一系列問題及解決方案。我在這部分主要介紹下java定義的內存模型以及內存模型是如何解決併發中的可見性、原子性、有序性問題。

內存模型

內存模型

1、java內存模型的目標是屏蔽各種硬件和操作系統的內存訪問差異,以實現讓java程序在各個平臺下都能達到一致的內存訪問效果。
2、內存模型主要目標是通過定義程序中各個變量的訪問規則來實現併發安全。這裏的訪問規則指的是虛擬機將變量存儲到內存以及從內存取出變量這種細節。這裏的變量指的是線程共享、存在線程競爭問題的變量(包括實例字段、靜態字段和構成數組對象的元素等)
3、內存模型規定了所有變量存儲在主內存中,而每個線程只能在自己的工作內存(類比於cache)中對變量進行操作,所以線程需要將變量從主內存拷貝一個副本到工作內存,當對副本變量進行修改時需要將變量更新回主內存,線程間變量值的傳遞需要通過主內存來完成,線程、主內存、工作內存之間存在交互關係,與此同時會出現緩存一致性問題。交互圖如下:
在這裏插入圖片描述
4、主內存、工作內存與Java內存區域對應的關係–主內存對應於Java堆中的對象實例數據部分,工作內存對應於虛擬機棧中的部分區域。物理角度來說,主內存對應於物理硬件的內存,虛擬機更可能讓工作內存優先存儲在cache中。

5、內存模型通過8個原子操作完成變量在內存間的交互操作,分別爲lock、unlock、read、load、use、assign、store、write(操作功能不在這詳述)read與load以及write和store之間需要順序執行,但無需連續執行。8種原子操作在執行時需要滿足一定的規則(不在這詳述)
在這裏插入圖片描述

原子性、可見性與有序性

Java內存模型在併發過程中通過處理好原子性、可見性、有序性三者來保證併發安全。volatile可保證可見性以及有序性。而鎖(synchronized以及Lock)是三者皆可保證。關鍵字final可保證可見性。volatile是最輕量級的同步機制,在大多數情況下,總開銷比鎖略低,性能相差無幾,選擇volatile還是鎖的唯一依據僅僅是volatile是否符合使用場景需求。(後面會敘述使用場景需求)

原子性

原子性指的是操作要麼都執行,不可中斷,要麼都不執行。例如一個++i操作,這看似是一個原子性操作,但其實它包含了三個獨立的操作,讀取i值,將值加1,再將值寫入i中,所以它不是一個原子性操作,非原子性操作在單線程中可能不會出錯,但在多線程中就會出現偏差。

原子性操作包括內存模型提供的八種原子操作,對基本數據類型的訪問讀寫也是具備原子性的,synchronized以及Lock可實現大範圍的原子操作,由於synchronized和Lock能夠保證任一時刻只有一個線程執行該代碼塊,那麼自然就不存在原子性問題了,從而保證了原子性。

可見性

可見性指的是當一個線程修改了共享變量的值,其它線程能夠立馬得到這個修改。Java內存模型通過在工作內存中修改後將修改的新值同步回主內存,在變量讀取前從主內存中將值刷新到工作內存中來完實現可見性。這在單線程中不會出錯,但在多線程中會出現錯誤。因爲值更新回主內存往往不會立刻得到執行,而且值在工作內存和主內存傳遞過程中耽擱了時間,可能出現讀值和寫值延遲。

public class thread_test  extends  Thread{
     private String name;
     private static int count=10;

    public thread_test(String name) {
        this.name = name;
    }
    @Override
    public void run(){
     for(int i=0;i<10;i++){
         if(count>0)
         {
             count--;
             System.out.println("線程"+name+"進行倒計時之後的count值"+count);
         }
         else{
             break;

         }
         try{
             sleep((int)Math.random()*10);
         }
         catch(InterruptedException e){
             e.printStackTrace();
         }
     }
   }
     public static void main(String[] args)
     {
         Thread t1=new thread_test("A");
         Thread t2=new thread_test("B");
         t1.start();
         t2.start();
     }
}
線程B進行倒計時之後的count值8
線程A進行倒計時之後的count值9
線程A進行倒計時之後的count值7
線程B進行倒計時之後的count值7
線程A進行倒計時之後的count值6
線程B進行倒計時之後的count值5
線程A進行倒計時之後的count值4
線程B進行倒計時之後的count值3
線程A進行倒計時之後的count值2
線程B進行倒計時之後的count值1
線程A進行倒計時之後的count值0

上述代碼前四行就可知因爲存在可見性問題,所以兩個線程讀取以及修改的值都出錯,出現了線程安全問題。線程A經過一次減操作,將新值9從工作內存傳回主內存,線程B從工作內存將值9從主內存讀到工作內存進行減操作,將值8寫回主內存,而此時線程A將主內存中的值8讀回工作內存,然後線程A和B都進行減操作,但沒有及時通知另一個線程它的更新,因此此時就出現可見性問題。

因此在多線程間,普通變量存在可見性問題,需要通過加鎖機制來保證變量的可見性。或者通過volatile來實現,volatile變量的特殊規則保證了新值立即同步到主內存,以及每次使用時立即從主內存刷新。

有序性

有序性即程序執行的順序按照代碼的先後順序執行。但Java中往往是線程內有序,而線程間無序,這是由於Java的指令重排序、主內存和工作內存同步延遲導致的。
(注:指令重排序指的是處理器爲了提高程序運行效率,可能會對輸入代碼進行優化,它不保證程序中各個語句的執行先後順序同代碼中的順序一致,但是它會保證程序最終執行結果和代碼順序執行的結果是一致的,因爲它不會對變量之間存在數據依賴的變量進行重排序)

Java提供了volatile和synchronized兩個關鍵字來保證線程之間操作的有序性,volatile通過禁止指令重排序來實現這點,而synchronized通過規定同一時刻只有一個線程能對變量進行上鎖,從而規定了線程只能串行的訪問變量來實現有序性。

但Java中如果所有的有序性都要依靠volatile和synchronized來完成那在Java中操作就會變得繁瑣、開銷大,Java也不會有這麼高的性能。在Java中,存在天然的先行發生關係,它無需任何同步器協助就已經存在,當滿足"先行發生原則時",則無需其它操作,操作即滿足有序性。

先行發生是JMM定義的兩項操作之間的偏序關係,與時間無關,它指的是在發生操作B之前,操作A的修改變量、發送消息、調用方法等行爲造成的結果能被操作B觀察到。

先行發生原則有八種,只要滿足以下八種,則無需其它同步手段就能保證有序性。
1、程序次序規則:一個線程內,按照代碼順序,書寫在前面的操作先行發生於書寫在後面的操作
2、鎖定規則:一個unLock操作先行發生於後面對同一個鎖的lock操作
3、volatile變量規則:對一個volatile變量的寫操作先行發生於後面對這個變量的讀操作
4、傳遞規則:如果操作A先行發生於操作B,而操作B又先行發生於操作C,則可以得出操作A先行發生於操作C
5、線程啓動規則:Thread對象的start()方法先行發生於此線程的每個一個動作
6、線程中斷規則:對線程interrupt()方法的調用先行發生於被中斷線程的代碼檢測到中斷事件的發生
7、線程終結規則:線程中所有的操作都先行發生於線程的終止檢測,我們可以通過Thread.join()方法結束、Thread.isAlive()的返回值手段檢測到線程已經終止執行
8、對象終結規則:一個對象的初始化完成先行發生於他的finalize()方法的開始

volatile

在Java多線程編程中,我們往往習慣用鎖機制來保障線程安全問題,儘管虛擬機對鎖進行了很大程度上的優化,但在某些情況下,volatile的同步性能的確比鎖機制更優,而且voatile變量讀操作的性能消耗與普通變量幾乎沒有什麼差別,寫操作稍微慢一點,因爲需要插入內存屏障指令來保證有序性。volatile是最輕量級的同步機制,在大多數情況下,總開銷比鎖略低,性能相差無幾,選擇volatile還是鎖的唯一依據僅僅是volatile是否符合使用場景需求。

因爲volatile可以保證可見性和有序性,但無法保證原子性,所以在不滿足原子性的場合下無法使用,因此在符合以下兩條規則的運算場景中,可以使用volatile關鍵字,其它場景需要使用鎖機制來保證原子性。
1、運算結果並不依賴變量的當前值或者能夠確保只有單一線程修改變量的值。
2、變量不需要與其他狀態共同參與不變約束。

volatile可以確保變量對所有線程的可見性和有序性。volatile型變量實現可見性的機制是通過其特殊規則保證了修改後的新值立馬更新同步到主內存,以及使用前都立即從主內存刷新。volatile實現有序性是通過禁止指令重排序優化來實現。volatile修飾的變量在複製後會進行一個相當於內存屏障的操作,指令重排序時不能將後面的指令重排序到屏障之前,這個操作還會將本cpu中的cache寫入內存,寫入操作引起別的cpu中的cache無效化。變量值寫入內存之後,意味着這個變量的操作已經完成,其它線程只能再其後對其進行操作。

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