1. 程式人生 > >從Java記憶體模型理解synchronized、volatile和final關鍵字

從Java記憶體模型理解synchronized、volatile和final關鍵字

        你是否真正理解並會用volatile, synchronized, final進行執行緒間通訊呢,如果你不能回答下面的幾個問題,那就說明你並沒有真正的理解:

        1、對volatile變數的操作一定具有原子性嗎?(原子操作是不需要synchronized來保護的,所謂原子操作是指不會被執行緒排程機制打斷的操作,這種操作一旦開始,就一直執行到結束,中間不會有任何執行緒切換)

        2、synchronized所謂的加鎖,鎖住的是什麼?

        3、final定義的變數不變的到底是什麼?

1、Java記憶體模型

        看Java記憶體模型之前,我們先來了解什麼是記憶體模型?

        在多處理器系統中,處理器通常有多級快取,因為這些快取離處理器更近並且可以儲存一部分資料,所以快取可以改善處理器獲取資料的速度和減少對共享內 存資料匯流排的佔用。快取雖然能極大的提高效能,但是同時也帶來了諸多挑戰。例如,當兩個處理器同時操作同一個記憶體地址的時候,該如何處理?這兩個處理器在 什麼條件下才能看到相同的值?

        對於處理器而言,一個記憶體模型就是定義一些充分必要的規範,這些規範使得其他處理器對記憶體的寫操作對當前處理器可見,或者當前處理器的寫操作對其他處理器可見。

        其他處理器對記憶體的寫一定發生在當前處理器對同一記憶體的讀之前,稱之為其他處理器對記憶體的寫對當前處理器可見。


        知道了記憶體模型,那麼應該可以更好的理解java記憶體模型。簡單的講,java記憶體模型指的就是一套規範,現在最新的規範為JSR-133。這套規範包含:

        1、執行緒之間如何通過記憶體通訊;

        2、執行緒之間通過什麼方式通訊才合法,才能得到期望的結果。

        我們已經知道 java 記憶體模型就是一套規範,那麼在這套規範中,規定的記憶體結構是什麼樣的呢?java記憶體模型中的記憶體結構如下圖所示:


        簡單的講,Java 記憶體模型將記憶體分為共享記憶體和本地記憶體。共享記憶體又稱為堆記憶體,指的就是執行緒之間共享的記憶體,包含所有的例項域、靜態域和陣列元素。每個執行緒都有一個私有的,只對自己可見的記憶體,稱之為本地記憶體

        共享記憶體中共享變數雖然由所有的執行緒共享,但是為了提高效率,執行緒並不直接使用這些變數,每個執行緒都會在自己的本地記憶體中儲存一個共享記憶體的副本,使用這個副本參與運算。由於這個副本的參與,導致了執行緒之間對共享記憶體的讀寫存在可見性問題

        為了方便執行緒之間的通訊,java 提供了 volatile, synchronized, final 三個關鍵字供我們使用,下面我們來看看如何使用它們進行執行緒間通訊。

2、volatile關鍵字

        volatile 定義的變數,特殊性在於:

        一個執行緒對 volatile 變數的寫一定對之後對這個變數的讀的執行緒可見。

        等價於

        一個執行緒對 volatile 變數的讀一定能看見在它之前最後一個執行緒對這個變數的寫。

        為了實現這些語義,Java 規定,(1)當一個執行緒要使用共享記憶體中的 volatile 變數時,如圖中的變數a,它會直接從主記憶體中讀取,而不使用自己本地記憶體中的副本。(2)當一個執行緒對一個 volatile 變數進行寫時,它會將這個共享變數的值重新整理到共享記憶體中。

        我們可以看到,其實 volatile 變數保證的是一個執行緒對它的寫會立即重新整理到主記憶體中,並置其它執行緒的副本為無效,它並不保證對 volatile 變數的操作都是具有原子性的。

public void add(){
     a++;         #1
 }
        等價於
public void add() {   
    temp = a;        
    temp = temp +1;  
    a = temp;         
 }
        程式碼1並不是一個原子操作,所以類似於 a++ 這樣的操作會導致併發資料問題。

        volatile 變數的寫被保證是可以被之後其他執行緒的讀看到的,因此我們可以利用它進行執行緒間的通訊。如:

volatile int a;
public void set(int b) {
    a = b; 
}
public void get() {    
    int i = a; 
}

        執行緒A執行set()後,執行緒B執行get(),相當於執行緒A向執行緒B傳送了訊息。

3、synchronized

        如果我們非要使用 a++ 這種複合操作進行執行緒間通訊呢?java 為我們提供了synchronized。
public synchronized void add() {
    a++; 
 }
        synchronized 使得它作用範圍內的程式碼對於不同執行緒是互斥的,並且執行緒在釋放鎖的時候會將共享變數的值重新整理到主記憶體中。

        我們可以利用這種互斥性來進行執行緒間通訊。看下面的程式碼:

public synchronized void add() {
    a++; 
}
public synchronized void get() {  
    int i = a; 
}

        當執行緒A執行 add(),執行緒B呼叫get(),由於互斥性,執行緒A執行完add()後,執行緒B才能開始執行get(),並且執行緒A執行完add(),釋放鎖的時候,會將a的值重新整理到共享記憶體中。因此執行緒B拿到的a的值是執行緒A更新之後的。

4、volatile和synchronized的比較

        根據以上的分析,我們可以發現volatile和synchronized有些相似。

        1、當執行緒對 volatile變數寫時,java 會把值重新整理到共享記憶體中;而對於synchronized,指的是當執行緒釋放鎖的時候,會將共享變數的值重新整理到主記憶體中。

        2、執行緒讀取volatile變數時,會將本地記憶體中的共享變數置為無效;對於synchronized來說,當執行緒獲取鎖時,會將當前執行緒本地記憶體中的共享變數置為無效。

        3、synchronized 擴大了可見影響的範圍,擴大到了synchronized作用的程式碼塊。

5、final變數

        final關鍵字可以修飾變數、方法和類,我們這裡只討論final修飾的變數。final變數的特殊之處在於:

        final 變數一經初始化,就不能改變其值。

        這裡的值對於一個物件或者陣列來說指的是這個物件或者陣列的引用地址。因此,一個執行緒定義了一個final變數之後,其他任意執行緒都可以拿到這個變數。但有一點需要注意的是,當這個final變數為物件或者陣列時,

        1、雖然我們不能將這個變數賦值為其他物件或者陣列的引用地址,但是我們可以改變物件的域或者陣列中的元素

        2、執行緒對這個物件變數的域或者資料的元素的改變不具有執行緒可見性。