鎖優化的背景
JDK5版本帶來了J.U.C包以及其他併發相關的技術,使得Java語言對於併發的支持更加完善。在這個基礎上,JDK6爲了更加高效的併發,Hotspot虛擬機的開發團隊花費了大量的精力去實現各種鎖優化的技術:自旋鎖、自適應自旋鎖、鎖消除、鎖膨脹、輕量級鎖、偏向鎖等。
自旋鎖與自適應自旋鎖
互斥同步對於性能最大的影響點在於線程阻塞導致用戶態和內核態切換所帶來的的性能消耗。同時一個現狀是:多數情況下,共享數據的鎖定狀態持續的時間都比較短。在這麼短的阻塞情況下,去阻塞線程帶往往是不值得的,尤其是當今多核處理器的現狀下。
因此,當遇到鎖競爭的情況下,我們可以暫時不阻塞後面的線程,而是讓他們不放棄處理器的資源,進行一個忙循環,來等待鎖的釋放。這項技術就是所謂的自旋鎖。
自旋鎖的好處是可以避免線程直接阻塞導致的性能消耗,但是自旋鎖並不能代替阻塞。如果鎖競爭十分激烈並且鎖佔用時間過長,線程將一直忙循環從而浪費處理器的資源。因此不能讓線程一直處於自旋中,必須有一個限度:線程自旋時間需要有限度;線程自旋次數需要有限度。當一個線程經過數次自旋依然沒有獲取到鎖時,應該進入到阻塞狀態。
Java中對於自旋鎖的具體優化方式是,線程自旋的時長和次數由前一次獲取鎖的自旋時間和獲取鎖的線程狀態決定的。如果前一次自旋時間比較短就獲得了鎖,虛擬機就會認爲本次也很有可能在較短的時間內獲取到鎖,進而允許本次自旋時間等待時間更長。如果通過自旋的方式獲取鎖的成功機率很低,那麼虛擬機也很有可能不讓線程進行自旋而是直接進入到阻塞狀態,避免白白浪費處理器資源。
鎖消除
鎖消除是指在程序運行情況下,有些代碼要求同步,但是虛擬機檢測到不存在共享數據競爭,從而消除掉鎖。鎖消除的技術實際上是基於逃逸分析的。如果判斷到一段代碼中涉及到的數據不會逃逸出去被其他線程訪問到,就可以認爲這些數據是線程私有的,自然就不存在鎖競爭的情況。
我們來看這一段代碼:
// 這段代碼沒有涉及到任何共享數據,只是一個普通的虛方法.
public String concatStr(String str1, String str2, String str3) {
synchronized(this) {
synchronized(this) {
return str1 + str2 + str3;
}
}
}
通過反編譯來看一下:
public java.lang.String concatStr(java.lang.String, java.lang.String, java.lang.String);
descriptor: (Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Ljava/lang/String;
flags: ACC_PUBLIC
Code:
stack=2, locals=8, args_size=4
0: aload_0
1: dup
2: astore 4
4: monitorenter
5: aload_0
6: dup
7: astore 5
9: monitorenter
10: new #2 // class java/lang/StringBuilder
13: dup
14: invokespecial #3 // Method java/lang/StringBuilder."<init>":()V
17: aload_1
18: invokevirtual #4 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
21: aload_2
22: invokevirtual #4 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
25: aload_3
26: invokevirtual #4 // Method java/lang/StringBuilder.append:(Ljava/lang/String;)Ljava/lang/StringBuilder;
29: invokevirtual #5 // Method java/lang/StringBuilder.toString:()Ljava/lang/String;
32: aload 5
34: monitorexit
35: aload 4
37: monitorexit
38: areturn
39: astore 6
41: aload 5
43: monitorexit
44: aload 6
46: athrow
47: astore 7
49: aload 4
51: monitorexit
52: aload 7
54: athrow
可以看到,通過靜態編譯之後,有兩對monitorenter-monitorexit。並沒有進行鎖消除啊~是的,靜態編譯並不會進行鎖消除,鎖消除是在程序運行時進行的。
鎖粗化
如果有一系列的操作都會對同一個對象進行加鎖和解鎖,那麼即使沒有鎖競爭,頻繁的進行互斥同步操作也會導致不必要的性能消耗。我們看這段代碼:
public Object lock = new Object();
public void loop(int n) {
for(int i = 0; i< n; i++) {
synchronized(lock) {
System.out.println(i);
}
}
}
以上程序在實際運行中會被虛擬機優化進行鎖粗化,等同於一下代碼:
public Object lock = new Object();
public void loop(int n) {
synchronized(lock) {
for(int i = 0; i< n; i++) {
System.out.println(i);
}
}
}
輕量級鎖
在JDK6之前,基於synchronized關鍵字進行同步時,是一個“重量級”操作,在JDK6時,引入了“輕量級”的概念,輕量級並不是要取代傳統的“重量級”。而是在所競爭沒有那麼激烈的情況下,採用輕量級鎖機制可以降低性能的消耗。
偏向鎖
偏向鎖也是JDK6引入的優化技術。它的目的是消除無鎖競爭時的同步原語,進一步提升程序性能。
關於輕量級鎖和偏向鎖的原理我們在介紹synchronized時講解。