天天看點

Java線程安全以及線程安全的實作方式和記憶體模型(JMM)(1)

一、了解幾個概念

1)臨界區:

臨界區指的是一個通路共用資源(例如:共用裝置或是共用存儲器)的程式片段,而這些共用資源又無法同時被多個線程通路的特性。當有線程進入臨界區段時,其他線程或是程序必須等待,有一些同步的機制必須在臨界區段的進入點與離開點實作,以確定這些共用資源是被互斥獲得使用

2)互斥量:

互斥量是一個可以處于兩态之一的變量:解鎖和加鎖。這樣,隻需要一個二進制位表示它,不過實際上,常常使用一個整型量,0表示解鎖,而其他所有的值則表示加鎖。互斥量使用兩個過程。當一個線程(或程序)需要通路臨界區時,它調用mutex_lock。如果該互斥量目前是解鎖的(即臨界區可用),此調用成功,調用線程可以自由進入該臨界區。

另一方面,如果該互斥量已經加鎖,調用線程被阻塞,直到在臨界區中的線程完成并調用mutex_unlock。如果多個線程被阻塞在該互斥量上,将随機選擇一個線程并允許它獲得鎖。

3)信号量:

信号量(Semaphore),有時被稱為信号燈,是在多線程環境下使用的一種設施,是可以用來保證兩個或多個關鍵代碼段不被并發調用。在進入一個關鍵代碼段之前,線程必須擷取一個信号量;一旦該關鍵代碼段完成了,那麼該線程必須釋放信号量。其它想進入該關鍵代碼段的線程必須等待直到第一個線程釋放信号量。為了完成這個過程,需要建立一個信号量VI,然後将Acquire Semaphore VI以及Release Semaphore VI分别放置在每個關鍵代碼段的首末端。确認這些信号量VI引用的是初始建立的信号量。

4)CAS操作(Compare-and-Swap):

CAS有3個操作數,記憶體值V,舊的預期值A,要修改的新值B。當且僅當預期值A和記憶體值V相同時,将記憶體值V修改為B

5)重排序:

編譯器和處理器”為了提高性能,而在程式執行時會對程式進行的重排序。它的出現是為了提高程式的并發度,進而提高性能!但是對于多線程程式,重排序可能會導緻程式執行的結果不是我們需要的結果!重排序分為“編譯器”和“處理器”兩個方面,而“處理器”重排序又包括“指令級重排序”和“記憶體的重排序”

二、Java記憶體模型(JMM)

線程與記憶體互動操作如下  

Java線程安全以及線程安全的實作方式和記憶體模型(JMM)(1)

所有的變量(執行個體字段,靜态字段,構成數組對象的 元素,不包括局部變量和方法參數)都存儲在主記憶體中,每個線程有自己的工作記憶體,線程的工作記憶體儲存被線程使用到變量的主記憶體副本拷貝。線程對變量的所有操作都必須在工作記憶體中進行,而不能直接讀寫主記憶體的變量。不同線程之間也不能直接通路對方工作記憶體中的變量,線程間變量值的傳遞通過主記憶體來完成。

1、Java記憶體模型定義了八種操作:

lock(鎖定):作用于主記憶體的變量,它把一個變量辨別為一個線程獨占的狀态;
unlock(解鎖):作用于主記憶體的變量,它把一個處于鎖定狀态的變量釋放出來,釋放後的變量才可以被其他線程鎖定;
read(讀取):作用于主記憶體的變量,它把一個變量的值從主記憶體傳送到線程中的工作記憶體,以便随後的load動作使用;
load(載入):作用于工作記憶體的變量,它把read操作從主記憶體中得到的變量值放入工作記憶體的變量副本中;
use(使用):作用于工作記憶體的變量,它把工作記憶體中一個變量的值傳遞給執行引擎;
assign(指派):作用于工作記憶體的變量,它把一個從執行引擎接收到的值指派給工作記憶體中的變量;
store(存儲):作用于工作記憶體的變量,它把工作記憶體中的一個變量的值傳送到主記憶體中,以便随後的write操作;
write(寫入):作用于主記憶體的變量,它把store操作從工作記憶體中得到的變量的值寫入主記憶體的變量中。      

2、volatile關鍵字作用:

1)、當寫一個volatile變量時,JMM會把該線程對應的本地記憶體中的共享變量值立即重新整理到主記憶體中。

2)、當讀一個volatile變量時,JMM會把該線程對應的本地記憶體設定為無效,直接從主記憶體中讀取共享變量。

3)、禁止指令重排序優化。

4)、volatile關鍵字不能保證在多線程環境下對共享資料的操作的正确性。可以使用在自己狀态改變之後需要立即通知所有線程的情況下,也就是說volatile不能保證線程同步,Java語言提供了一種稍弱的同步機制,即volatile變量,用來確定将變量的更新操作通知到其他線程,這就是所謂的線程可見性,當把變量聲明為volatile類型後,編譯器與運作時都會注意到這個變量是共享的,是以不會将該變量上的操作與其他記憶體操作一起重排序。volatile變量不會被緩存在寄存器或者對其他處理器不可見的地方,是以在讀取volatile類型的變量時總會傳回最新寫入的值,是以volatile具有可見性。對一個volatile變量的讀,總是能看到(任意線程)對這個volatile變量最後的寫入,然後synchronized也是具有可見性。

5)、volatile的原子性:對任意單個volatile變量的讀/寫具有原子性,但類似于volatile++這種複合操作不具有原子性,是以volatile能保證可見性,不能保證原子性。

了解volatile特性的一個好方法是:把對volatile變量的單個讀/寫,看成是使用同一個螢幕鎖對這些單個讀/寫操作做了同步

class VolatileFeaturesExample {
    volatile long vl = 0L;  //使用volatile聲明64位的long型變量
 
    public void set(long l) {
        vl = l;   //單個volatile變量的寫
    }
 
    public void getAndIncrement () {
        vl++;    //複合(多個)volatile變量的讀/寫
    }
 
 
    public long get() {
        return vl;   //單個volatile變量的讀
    }
}      

假設有多個線程分别調用上面程式的三個方法,這個程式在語意上和下面程式等價:

class VolatileFeaturesExample {
    long vl = 0L;               // 64位的long型普通變量
 
    public synchronized void set(long l) {     //對單個的普通 變量的寫用同一個螢幕同步
        vl = l;
    }
 
    public void getAndIncrement () { //普通方法調用
        long temp = get();           //調用已同步的讀方法
        temp += 1L;                  //普通寫操作
        set(temp);                   //調用已同步的寫方法
    }
    public synchronized long get() { 
    //對單個的普通變量的讀用同一個螢幕同步
        return vl;
    }
}      

3 volatile和synchronized差別

1)、volatile僅能使用在變量級别;synchronized則可以使用在變量、方法和類級别;

2)、volatile讀資料直接從主存中讀取;synchronized則是鎖定目前變量,隻有目前線程可以通路該變量,其他線程被阻塞住。

3)、volatile能保證可見性,不能保證原子性;而synchronized則可以保證可見性和原子性;

4)、volatile不會造成線程的阻塞;synchronized可能會造成線程的阻塞。

5)、volatile标記的變量不會被編譯器優化進行指令重排列;synchronized标記的變量可以被編譯器優化進行指令重排列。