《併發編程》--19.虛擬機內的鎖優化

首先要介紹下對象頭,在JVM中,每個對象都有一個對象頭。
Mark Word,對象頭的標記,32位(32位系統)。
描述對象的hash、鎖信息,垃圾回收標記,年齡
還會保存指向鎖記錄的指針,指向monitor的指針,偏向鎖線程ID等。
簡單來說,對象頭就是要保存一些系統性的信息。

1 偏向鎖

所謂的偏向,就是偏心,即鎖會偏向於當前已經佔有鎖的線程 。
大部分情況是沒有競爭的(某個同步塊大多數情況都不會出現多線程同時競爭鎖),所以可以通過偏向來提高性能。即在無競爭時,之前獲得鎖的線程再次獲得鎖時,會判斷是否偏向鎖指向我,那麼該線程將不用再次獲得鎖,直接就可以進入同步塊。
偏向鎖的實施就是將對象頭Mark的標記設置爲偏向,並將線程ID寫入對象頭Mark
當其他線程請求相同的鎖時,偏向模式結束
JVM默認啓用偏向鎖 -XX:+UseBiasedLocking
在競爭激烈的場合,偏向鎖會增加系統負擔(每次都要加一次是否偏向的判斷)

偏向鎖的例子:

package test;
 
import java.util.List;
import java.util.Vector;
 
public class Test {
 	 public static List<Integer> numberList = new Vector<Integer>();
 
 	 public static void main(String[] args) throws InterruptedException {
	 long begin = System.currentTimeMillis();
	 int count = 0;
	 int startnum = 0;
 	 while (count < 10000000) {
 		numberList.add(startnum);
 		startnum += 2;
		count++;
	 }
	 long end = System.currentTimeMillis();
 	 System.out.println(end - begin);
    }
}
Vector是一個線程安全的類,內部使用了鎖機制。每次add都會進行鎖請求。上述代碼只有main一個線程再反覆add請求鎖。
使用如下的JVM參數來設置偏向鎖:
-XX:+UseBiasedLocking -XX:BiasedLockingStartupDelay=0
BiasedLockingStartupDelay表示系統啓動幾秒鐘後啓用偏向鎖。默認爲4秒,原因在於,系統剛啓動時,一般數據競爭是比較激烈的,此時啓用偏向鎖會降低性能。
由於這裏爲了測試偏向鎖的性能,所以把延遲偏向鎖的時間設置爲0。
此時輸出爲9209
下面關閉偏向鎖:
-XX:-UseBiasedLocking
輸出爲9627
一般在無競爭時,啓用偏向鎖性能會提高5%左右。

2 輕量級鎖

Java的多線程安全是基於Lock機制實現的,而Lock的性能往往不如人意。
原因是,monitorenter與monitorexit這兩個控制多線程同步的bytecode原語,是JVM依賴操作系統互斥(mutex)來實現的。
互斥是一種會導致線程掛起,並在較短的時間內又需要重新調度回原線程的,較爲消耗資源的操作。
爲了優化Java的Lock機制,從Java6開始引入了輕量級鎖的概念。
輕量級鎖(Lightweight Locking)本意是爲了減少多線程進入互斥的機率,並不是要替代互斥。
它利用了CPU原語Compare-And-Swap(CAS,彙編指令CMPXCHG),嘗試在進入互斥前,進行補救。
如果偏向鎖失敗,那麼系統會進行輕量級鎖的操作。它存在的目的是儘可能不用動用操作系統層面的互斥,因爲那個性能會比較差。因爲JVM本身就是一個應用,所以希望在應用層面上就解決線程同步問題。
總結一下就是輕量級鎖是一種快速的鎖定方法,在進入互斥之前,使用CAS操作來嘗試加鎖,儘量不要用操作系統層面的互斥,提高了性能。
那麼當偏向鎖失敗時,輕量級鎖的步驟:
1.將對象頭的Mark指針保存到鎖對象中(這裏的對象指的就是鎖住的對象,比如synchronized (this){},this就是這裏的對象)。
lock->set_displaced_header(mark);
2.將對象頭設置爲指向鎖的指針(在線程棧空間中)。

if (mark == (markOop) Atomic::cmpxchg_ptr(lock, obj()->mark_addr(),mark)) {  
 	TEVENT (slow_enter: release stacklock) ;  
	return ; 
 }

lock位於線程棧中。所以判斷一個線程是否持有這把鎖,只要判斷這個對象頭指向的空間是否在這個線程棧的地址空間當中。
如果輕量級鎖失敗,表示存在競爭,升級爲重量級鎖(常規鎖),就是操作系統層面的同步方法。在沒有鎖競爭的情況,輕量級鎖減少傳統鎖使用OS互斥量產生的性能損耗。在競爭非常激烈時(輕量級鎖總是失敗),輕量級鎖會多做很多額外操作,導致性能下降。

3 自旋鎖

當競爭存在時,因爲輕量級鎖嘗試失敗,之後有可能會直接升級成重量級鎖動用操作系統層面的互斥。也有可能再嘗試一下自旋鎖。
如果線程可以很快獲得鎖,那麼可以不在OS層掛起線程,讓線程做幾個空操作(自旋),並且不停地嘗試拿到這個鎖(類似tryLock),當然循環的次數是有限制的,當循環次數達到以後,仍然升級成重量級鎖。所以在每個線程對於鎖的持有時間很少時,自旋鎖能夠儘量避免線程在OS層被掛起。
JDK1.6中-XX:+UseSpinning開啓
JDK1.7中,去掉此參數,改爲內置實現

如果同步塊很長,自旋失敗,會降低系統性能。如果同步塊很短,自旋成功,節省線程掛起切換時間,提升系統性能。

4 總結

上述的鎖不是Java語言層面的鎖優化方法,是內置在JVM當中的。
首先偏向鎖是爲了避免某個線程反覆獲得/釋放同一把鎖時的性能消耗,如果仍然是同個線程去獲得這個鎖,嘗試偏向鎖時會直接進入同步塊,不需要再次獲得鎖。
而輕量級鎖和自旋鎖都是爲了避免直接調用操作系統層面的互斥操作,因爲掛起線程是一個很耗資源的操作。
爲了儘量避免使用重量級鎖(操作系統層面的互斥),首先會嘗試輕量級鎖,輕量級鎖會嘗試使用CAS操作來獲得鎖,如果輕量級鎖獲得失敗,說明存在競爭。但是也許很快就能獲得鎖,就會嘗試自旋鎖,將線程做幾個空循環,每次循環時都不斷嘗試獲得鎖。如果自旋鎖也失敗,那麼只能升級成重量級鎖。
可見偏向鎖,輕量級鎖,自旋鎖都是樂觀鎖。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章