1. 程式人生 > >精進之路之volatile

精進之路之volatile

volatile

首先了解下Java 記憶體模型中的可見性、原子性和有序性。

可見性:

  可見性是一種複雜的屬性,因為可見性中的錯誤總是會違揹我們的直覺。通常,我們無法確保執行讀操作的執行緒能適時地看到其他執行緒寫入的值,有時甚至是根本不可能的事情。為了確保多個執行緒之間對記憶體寫入操作的可見性,必須使用同步機制。

  可見性,是指執行緒之間的可見性,一個執行緒修改的狀態對另一個執行緒是可見的。也就是一個執行緒修改的結果。另一個執行緒馬上就能看到。比如:用volatile修飾的變數,就會具有可見性。volatile修飾的變數不允許執行緒內部快取和重排序,即直接修改記憶體。所以對其他執行緒是可見的。但是這裡需要注意一個問題,volatile只能讓被他修飾內容具有可見性

但不能保證它具有原子性。比如 volatile int a = 0;之後有一個操作 a++;這個變數a具有可見性,但是a++ 依然是一個非原子操作,也就是這個操作同樣存線上程安全問題。

  在 Java 中 volatile、synchronized 和 final 實現可見性

原子性:

  原子是世界上的最小單位,具有不可分割性。比如 a=0;(a非long和double型別) 這個操作是不可分割的,那麼我們說這個操作時原子操作。再比如:a++; 這個操作實際是a = a + 1;是可分割的,所以他不是一個原子操作。非原子操作都會存線上程安全問題,需要我們使用同步技術(sychronized)來讓它變成一個原子操作。一個操作是原子操作,那麼我們稱它具有原子性。java的concurrent包下提供了一些原子類,我們可以通過閱讀API來了解這些原子類的用法。比如:AtomicInteger、AtomicLong、AtomicReference等。

  在 Java 中 synchronized 和在 lock、unlock 中操作保證原子性。

有序性:

  Java 語言提供了 volatile 和 synchronized 兩個關鍵字來保證執行緒之間操作的有序性,volatile 是因為其本身包含“禁止指令重排序”的語義,synchronized 是由“一個變數在同一個時刻只允許一條執行緒對其進行 lock 操作”這條規則獲得的,此規則決定了持有同一個物件鎖的兩個同步塊只能序列執行。

 

 volatile具備兩種特性,第一就是保證共享變數對所有執行緒的可見性。將一個共享變數宣告為volatile後,會有以下效應:

    1.當寫一個volatile變數時,JMM會把該執行緒對應的本地記憶體中的變數強制重新整理到主記憶體中去;

 

    2.這個寫會操作會導致其他執行緒中的快取無效。

 

Java語言提供了一種稍弱的同步機制,即volatile變數,用來確保將變數的更新操作通知到其他執行緒。當把變數宣告為volatile型別後,編譯器與執行時都會注意到這個變數是共享的,因此不會將該變數上的操作與其他記憶體操作一起重排序。volatile變數不會被快取在暫存器或者對其他處理器不可見的地方,因此在讀取volatile型別的變數時總會返回最新寫入的值。

  在訪問volatile變數時不會執行加鎖操作,因此也就不會使執行執行緒阻塞,因此volatile變數是一種比sychronized關鍵字更輕量級的同步機制。

 

 

  當對非 volatile 變數進行讀寫的時候,每個執行緒先從記憶體拷貝變數到CPU快取中。如果計算機有多個CPU,每個執行緒可能在不同的CPU上被處理,這意味著每個執行緒可以拷貝到不同的 CPU cache 中。

 

  而宣告變數是 volatile 的,JVM 保證了每次讀變數都從記憶體中讀,跳過 CPU cache 這一步。

 

當一個變數定義為 volatile 之後,將具備兩種特性:

 

  1.保證此變數對所有的執行緒的可見性,這裡的“可見性”,如本文開頭所述,當一個執行緒修改了這個變數的值,volatile 保證了新值能立即同步到主記憶體,以及每次使用前立即從主記憶體重新整理。但普通變數做不到這點,普通變數的值線上程間傳遞均需要通過主記憶體來完成。

 

  2.禁止指令重排序優化。有volatile修飾的變數,賦值後多執行了一個“load addl $0x0, (%esp)”操作,這個操作相當於一個記憶體屏障(指令重排序時不能把後面的指令重排序到記憶體屏障之前的位置),只有一個CPU訪問記憶體時,並不需要記憶體屏障;(什麼是指令重排序:是指CPU採用了允許將多條指令不按程式規定的順序分開發送給各相應電路單元處理)。

 

volatile 效能:

 

  volatile 的讀效能消耗與普通變數幾乎相同,但是寫操作稍慢,因為它需要在原生代碼中插入許多記憶體屏障指令來保證