java 執行緒——synchronized 和 volatile 關鍵字
synchronized 和 volatile 關鍵字
原子性、可見性、有序性
在講這兩個關鍵字之前,我們先來看一下幾個概念
原子性
原子性是指一個操作時不可中斷的,要麼全部執行成功,要麼全部執行失敗,即使在多個執行緒一起執行的時候,一個操作一旦開始,就不會被其他執行緒所幹擾,我們大致可以認為基本資料型別的訪問讀寫是具備原子性的(long 和 double 例外,這種情況很少,幾乎不會發生)。
但是如果在某個場景下需要更大範圍的原子性保證,java 記憶體還提供了lock 和 unlock 操作來滿足這種需求,儘管虛擬機器未把lock和 unlock 操作直接開放給使用者使用,但是卻提供了更高層次的位元組碼指令monitorenter 和moniterexit 來隱士地使用這兩個操作,這兩個位元組碼指令反映到java 程式碼中就是同步塊——synchronized 關鍵字,因此在synchronized塊之間的操作也具備原子性。
可見性
可見性是指當一個執行緒修改了共享變數的值,其它執行緒能夠立即得知這個修改。java記憶體模型是通過在修改變數後將新值同步回主記憶體,在變數讀取前從主記憶體重新整理變數值這種依賴主記憶體作為傳遞媒介的方式來實現可見性的。
有序性
java 程式中天然的有序性可以總結為一句話:如果在本執行緒內觀察,所有的操作都是有序的,如果在一個執行緒中觀察另一個執行緒,所有的操作都是無序的。前半句是指“執行緒內表現為序列的語義”,後半句是指“指令重排序現象”和“工作記憶體與主記憶體同步延遲的現象”。
java 語言提供了volatile 和sychronized兩個關鍵字來保證執行緒之間操作的有序性。volatile 關鍵字本身就包含了禁止指令重排序的語義,而sychronized 則是由“一個變數在同一個時刻只允許一條執行緒對其進行lock操作”這條規則獲得的,這條規則決定了持有同一個鎖的兩個同步塊只能序列地進入。
synchronized 關鍵字
synchronized 具有原子性、可見性、有序性
synchronized 關鍵字相當於拿到了一把鎖,這保證了它的原子性。根據synchronized 關鍵字的位置不同,它鎖住的物件也不同,具體可以分為以下三類。
加在程式碼塊上
public void add(Object obj){
/**
* 加在程式碼塊上
*/
synchronized (obj){
//do something……
}
}
當synchronized 加在程式碼塊上,它鎖住的物件就是括號裡面的物件,在上面的例子中指的就是obj 物件,當括號裡面為this時,表示鎖住的是當前物件。
class test1 implements Runnable{
int count = 0;
public void run() {
for(int i = 0;i < 10;i++){
synchronized(this){
try {
Thread.currentThread().sleep(500);
} catch (InterruptedException e) {
e.printStackTrace();
}
count++;
System.out.println(Thread.currentThread().getName()+":"+count);
}
}
}
}
我們對這個類來進行反彙編。
synchronized 加在同步程式碼塊上,使用的是monitorenter 和 monitorexit 指令,monitorenter 是同步程式碼塊開始的時候,物件開始嘗試獲取鎖,若物件拿到鎖,程式碼塊開始被執行,若鎖被其它執行緒佔有,執行緒進入阻塞狀態,直到其它執行緒釋放鎖。monitorexit 是在同步程式碼塊結束的時候,進行釋放鎖的操作,值得注意的是編譯器將會確保無論方法通過何種方式完成,方法中呼叫過的每條 monitorenter 指令都有執行其對應 monitorexit 指令,而無論這個方法是正常結束還是異常結束。為了保證在方法異常完成時 monitorenter 和 monitorexit 指令依然可以正確配對執行,編譯器會自動產生一個異常處理器,這個異常處理器宣告可處理所有的異常,它的目的就是用來執行 monitorexit 指令。從位元組碼中也可以看出多了一個monitorexit指令,它就是異常結束時被執行的釋放monitor 的指令。
加在方法上
/**
* 加在方法上,鎖的是當前物件
*/
public synchronized void update(){
//do something……
}
synchronized 加在方法上,鎖住的是當前物件。
加在靜態方法上
/**
* 加在靜態方法上,鎖的是類,靜態存在方法區
*/
public static synchronized void del(){
//do something……
}
volatile 關鍵字
當一個變數定義為volatile 後,它將具備兩種特性
- 可見性:保證此變數對所有執行緒的可見性
- 有序性:禁止指令重排序優化
可見性:這裡的可見性是指當一個執行緒修改了這個變數的值,新值對其他執行緒來說是可以立即得知的。
在寫的時候,即修改變數 1. 將修改的變數的副本寫入主記憶體 2. 其它執行緒的副本置為無效
讀的時候 先判斷 volatile 關鍵字修飾的變數副本是否有效,有效直接讀取 反之,則到主記憶體獲取最新值
由於volatile 變數只能保證可見性,在不符合以下兩條規則的運算場景中,我們仍然要通過加鎖來保證原子性。
- 運算結果並不依賴變數當前值,或者能夠確保只有單一執行緒修改變數的值。
- 變數不需要與其它的狀態變數共同參與不變約束
有序性:程式執行的順序按照程式碼的先後順序來執行
我們先來看一下指令重排序是什麼,在下面這段程式碼中,語句1 和語句2 並沒有依賴關係,也就是說它們無論哪個先執行對於結果來說都沒有什麼影響,所以這兒可能會發生指令重排序。
指令重排序就是cpu 為了提高效率,可能會對輸入程式碼進行優化,它不保證程式中各個語句的執行先後順序同程式碼中的順序一致,但是它會保證程式最終執行結果和程式碼順序執行的結果是一致的。
int i = 0;
boolean flag = false;
i = 1; //語句1
flag = true; //語句2
這是在單執行緒中,在多執行緒中指令重排序可能會產生錯誤的結果。
在下面的程式碼中,執行緒 1 裡面語句1 和語句2 沒有依賴關係,所以有可能當語句2 執行完之後語句1 還未執行,但這個時候執行緒2 檢測到了inited為false,退出了sleep狀態,執行下面的程式碼,這個時候就可能會出錯。
//執行緒1:
context = loadContext(); //語句1
inited = true; //語句2
//執行緒2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);
volatile 關鍵字就避免了指令的重排序,那麼它是如何實現這個操作的呢。
volatile 變數在賦值後多執行了一個lock 操作,這個操作相當於設定了一個記憶體屏障(指令重排序時不能把後面的指令重排序的記憶體屏障之前的位置)。lock的作用是使得本cpu的Cache 寫入了記憶體,該寫入的動作也會引起別的Cpu或者別的核心無效化其Cache。因此,lock指令把修改同步到記憶體時,意味著所有之前的操作都已經執行完成,這樣便形成了“指令重排序無法越過記憶體屏障”的效果。
synchronized 和 volatile 的區別
- volatile本質是在告訴jvm當前變數在暫存器(工作記憶體)中的值是不確定的,需要從主存中讀取; synchronized則是鎖定當前變數,只有當前執行緒可以訪問該變數,其他執行緒被阻塞住。
- volatile僅能使用在變數級別;synchronized則可以使用在變數、方法、和類級別的。
- volatile僅能實現變數的修改可見性,不能保證原子性;而synchronized則可以保證變數的修改可見性和原子性
- volatile不會造成執行緒的阻塞;synchronized可能會造成執行緒的阻塞。
- volatile標記的變數不會被編譯器優化;synchronized標記的變數可以被編譯器優化