1. 程式人生 > >Java並發基礎--volatile關鍵字

Java並發基礎--volatile關鍵字

read 發生 load vat 只有一個 分享圖片 info 沒有 虛擬

一、java內存模型

1.java內存模型

技術分享圖片

程序運行過程中的臨時數據是存放在主存(物理內存)中,但是現代計算機CPU的運算能力和速度非常的高效,從內存中讀取和寫入數據的速度跟不上CPU的處理速度,在這種情況下,CPU高速緩存應運而生。基於高速緩存的存儲交互很好地解決了處理器與內存的速度矛盾,但是出現了一個新的問題:目前計算機多為多核CPU或多處理器,每個處理器都有自己的高速緩存,但是這些處理器又共享同一主存。java內存模型主要是定義了java程序中各個變量的訪問規則,這裏的變量與java編程中變量不是同一個概念,包括:實例字段、靜態字段和構成數組對象的元素,但是不包括局部變量與方法參數,後者是線程私有的,java內存模型規定:

(1)所有的變量存儲在主內存中,每個線程都有自己的工作內存,線程的工作內存保存了該線程使用到的變量,並且這些變量是從主內存中對應變量的拷貝

(2)線程對變量的所有操作(讀取、賦值)都必須在工作內存中進行,而不能直接讀寫主內存中的變量

(3)不同線程之間無法直接訪問彼此的工作內存,線程間的變量值的傳遞需要借助主內存。

2.java工作內存與主內存的交互

lock(鎖定):作用於主內存的變量,把一個變量標識為一條線程獨占狀態。
unlock(解鎖):作用於主內存變量,把一個處於鎖定狀態的變量釋放出來,釋放後的變量才可以被其他線程鎖定。
read(讀取):作用於主內存變量,把一個變量值從主內存傳輸到線程的工作內存中,以便隨後的load動作使用


load(載入):作用於工作內存的變量,它把read操作從主內存中得到的變量值放入工作內存的變量副本中。
use(使用):作用於工作內存的變量,把工作內存中的一個變量值傳遞給執行引擎,每當虛擬機遇到一個需要使用變量的值的字節碼指令時將會執行這個操作。
assign(賦值):作用於工作內存的變量,它把一個從執行引擎接收到的值賦值給工作內存的變量,每當虛擬機遇到一個給變量賦值的字節碼指令時執行這個操作。
store(存儲):作用於工作內存的變量,把工作內存中的一個變量的值傳送到主內存中,以便隨後的write的操作。
write(寫入):作用於主內存的變量,它把store操作從工作內存中一個變量的值傳送到主內存的變量中。

二、基本概念

1.可見性:java多線程之間的可見性,一個線程修改的某個對象的狀態對另外一個線程是可見的。java中經常使用同步或者volatile來保證變量的可見性。

2.原子性:即一個操作或者多個操作 要麽全部執行並且執行的過程不會被任何因素打斷,要麽就都不執行。原子代表不可分割性,例如:int a=0,這個操作是不可分割的,反之,a++這個操作是可以切割的
可以分為a=a+1,讀取內存中a的值,讀取a的值後進行加1操作,把結果值寫入內存。

3.有序性:即程序執行的順序按照代碼的先後順序執行。在Java內存模型中,允許編譯器和處理器對指令進行重排序,但是重排序過程不會影響到單線程程序的執行,卻會影響到多線程並發執行的正確性。

4.重排序:在執行程序時為了提高性能,編譯器和處理器經常會對指令進行重排序,重排序分成三種類型:

  • 編譯器優化的重排序。編譯器在不改變單線程程序語義放入前提下,可以重新安排語句的執行順序

  • 指令級並行的重排序。現代處理器采用了指令級並行技術來將多條指令重疊執行
  • 內存系統的重排序。由於處理器使用緩存和讀寫緩沖區,這使得加載和存儲操作看上去可能是在亂序執行

三、volatile

1.基本概念

Java語言提供了一種稍弱的同步機制,即volatile變量,用來確保將變量的更新操作通知到其他線程。當把變量聲明為volatile類型後,編譯器與運行時都會註意到這個變量是共享的,因此不會將該變量上的操作與其他內存操作一起重排序。volatile變量不會被緩存在寄存器或者對其他處理器不可見的地方,因此在讀取volatile類型的變量時總會返回最新寫入的值。當對非 volatile 變量進行讀寫的時候,每個線程先從內存拷貝變量到CPU緩存中。如果計算機有多個CPU,每個線程可能在不同的CPU上被處理,這意味著每個線程可以拷貝到不同的 CPU cache 中。而聲明變量是 volatile 的,JVM 保證了每次讀變量都從內存中讀,跳過 CPU cache 這一步。

2.基本使用

 1 public class DoubleCheck{
 2 
 3     private static volatile DoubleCheck instance;
 4 
 5     private DoubleCheck(){}
 6 
 7     public static DoubleCheck getInstance(){
 8 
 9         //第一次檢測
10         if (instance==null){
11             //同步
12             synchronized (DoubleCheck.class){
13                 if (instance == null){
14                     //多線程環境下可能會出現問題的地方
15                     instance = new DoubleCheck();
16                 }
17             }
18         }
19         return instance;
20     }
21 }

上述代碼一個經典的單例的雙重檢測的代碼,在單線程環境下這樣寫並沒有什麽問題,但如果在多線程環境下就可能出現線程安全問題。原因在於某一個線程執行到第一次檢測,讀取到的instance不為null時,instance的引用對象可能沒有完成初始化,而通過volatile修飾後,就能很好的解決非線程安全的問題。

3.總結

①、保證此變量對所有的線程的可見性,這裏的“可見性”,如本文開頭所述,當一個線程修改了這個變量的值,volatile 保證了新值能立即同步到主內存,以及每次使用前立即從主內存刷新

②、禁止指令重排序優化。有volatile修飾的變量,賦值後多執行了一個“load addl $0x0, (%esp)”操作,這個操作相當於一個內存屏障(指令重排序時不能把後面的指令重排序到內存屏障之前的位置),只有一個CPU訪問內存時,並不需要內存屏障

③、volatile的讀性能消耗與普通變量幾乎相同,但是寫操作稍慢,因為它需要在本地代碼中插入許多內存屏障指令來保證處理器不發生亂序執行。

④、volatile變量,本質上是通過內存屏障來實現其可見性和禁止重排優化。內存屏障是硬件層的概念,不同的硬件平臺實現內存屏障的手段並不是一樣,java通過屏蔽這些差異,統一由jvm來生成內存屏障的指令。內存屏障有兩個作用:阻止屏障兩側的指令重排序;強制把寫緩沖區/高速緩存中的臟數據等寫回主內存,讓緩存中相應的數據失效。

Java並發基礎--volatile關鍵字