這是一篇關于java鎖的總結的開端,后續會單獨對部分鎖的原理進行展開。內容大多來自《深入理解Java虛擬機》、《Java并發編程實戰》和網絡上。
公平鎖和非公平鎖
顧名思義,一個是不可搶占嚴格按照先到先得的鎖,一個是可搶占的不公平的鎖。
公平鎖是指多個線程在等待同一個鎖時,必須按照申請鎖的先后順序來一次獲得鎖。公平鎖的好處是等待鎖的線程不會餓死,但是整體效率相對低一些;
非公平鎖的好處是整體效率相對高一些,但是有些線程可能會餓死或者要等很久才會獲得鎖。如果在某個時刻有線程需要獲取鎖,而這個時候剛好鎖可用,那么這個線程會直接搶占,而這時阻塞在等待隊列的線程則不會被喚醒。
公平鎖可以使用new ReentrantLock(true)實現。
可重入鎖
可重入鎖,也叫做遞歸鎖,指的是同一線程外層函數獲得鎖之后 ,內層遞歸函數仍然有獲取該鎖的代碼,但不受影響。
在JAVA環境下 ReentrantLock 和synchronized 都是可重入鎖。可重入鎖最大的作用是避免死鎖。
重入的一種實現方法是:為每個鎖關聯一個獲取計數器和一個所有者線程。當計數器值為0時,這個鎖就被認為是沒有被任何線程持有。當線程請求一個未被持有的鎖時,JVM記下鎖的持有者,并將計數器置1,如果同一個線程再次獲取到鎖,計數器遞增,當線程退出同步代碼塊時,計數器會相應地遞減。當計數器值為0時,這個鎖將被釋放。
不然當子類改寫了父類的synchronized方法,然后調用父類中的方法,此時如果沒有可重入鎖,那么這段代碼將會產生死鎖,如:
public class Widget {
public synchronized void doSomething() {
...
}
}
public class LoggingWidget extends Widget {
public synchronized void doSomething() {
super.doSomething();
}
}
鎖消除
鎖消除是虛擬機在運行時,對一些代碼上要求同步,但是被檢測到不可能存在共享數據競爭的鎖進行消除。鎖消除的主要判斷依據是來源于逃逸分析的數據支持,如果判斷在一段代碼中,堆上的所有數據都不會逃逸出去從而能被其他線程訪問到,那就可以把他們當做棧上數據對待,認為他們是線程私有的,同步加鎖自然就無需進行。看例子:
public String concatString(String s1, String s2, String s3) {
StringBuffer sb StringBuffer();
sb.append(s1);
sb.append(s2);
sb.append(s3);
return sb.toString();
}
StringBuffer append方法的內部加了同步關鍵字:
public synchronized StringBuffer append(Object obj) {
toStringCache = null;
super.append(String.valueOf(obj));
return this;
}
也就是說在concatString()方法中涉及了同步操作。但是虛擬機觀察sb變量的的作用域被限制在方法的內部,也就是sb的所有引用不會“逃逸”到concatString之外,其他線程無法訪問到它。因此,雖然這里有鎖,但是可以被安全的消除,在即時編譯之后,這段代碼就會忽略掉所有的同步而直接執行了。
鎖粗化
原則上,我們在編寫代碼的時候,總是推薦將同步塊的作用范圍限制的盡量小——只在共享數據的實際作用域中才進行同步,這樣是為了使得需要同步的操作數量盡可能變小,如果存在鎖競爭,那等待的線程也能盡快拿到鎖。大部分情況下,這些都是正確的。但是,如果一些列的聯系操作都是同一個對象反復加上和解鎖,甚至加鎖操作是出現在循環體中的,那么即使沒有線程競爭,頻繁地進行互斥同步操作也導致不必要的性能損耗。
舉個案例,類似鎖消除的concatString()方法。如果StringBuffer sb = new StringBuffer();定義在方法體之外,那么就會有線程競爭,但是每個append()操作都對同一個對象反復加鎖解鎖,那么虛擬機探測到有這樣的情況的話,會把加鎖同步的范圍擴展到整個操作序列的外部,即擴展到第一個append()操作之前和最后一個append()操作之后,這樣的一個鎖范圍擴展的操作就稱之為鎖粗化。
類鎖和對象鎖
類鎖:在方法上加上static synchronized的鎖,或者synchronized(xxx.class)的鎖,鎖的范圍是整個類class。如下代碼中的m1和m2:
對象鎖:鎖對象是當前類對象或者自定義鎖對象,參考m3,m4,m5。
public class LockClass {
public Object lock = Object();
//類鎖
public static synchronized m1(){}
public m2(){ synchronized(LockClass.class){}}
//對象鎖
public synchronized m3(){}
public m4() { synchronized(){} }
public m5() { synchronized(object1){} }
}
自旋鎖
Java的線程是映射到操作系統的原生線程之上的,如果要阻塞或喚醒一個線程,都需要操作系統來幫忙完成,這就需要從用戶態轉換到核心態中,因此狀態裝換需要耗費很多的處理器時間,對于代碼簡單的同步塊(如被synchronized修飾的getter()和setter()方法),狀態轉換消耗的時間有可能比用戶代碼執行的時間還要長。
在許多應用上,共享數據的鎖定狀態只會持續很短的一段時間,為了這段時間取掛起和恢復現場并不值得。
如果物理機器有一個以上的處理器,能讓兩個或以上的線程同時并行執行,我們就可以讓后面請求鎖的那個線程“稍等一下“,但不放棄處理器的執行時間,看看持有鎖的線程是否很快就會釋放鎖。為了讓線程等待,我們只需讓線程執行一個忙循環(自旋),這項技術就是所謂的自旋鎖。
自旋等待不能代替阻塞。自旋等待本身雖然避免了線程切換的開銷,但它是要占用處理器時間的,因此,如果鎖被占用的時間很短,自旋當代的效果就會非常好,反之,如果鎖被占用的時間很長,那么自旋的線程只會白白浪費處理器資源。因此,自旋等待的時間必須要有一定的限度,如果自旋超過了限定次數(默認是10次,可以使用-XX:PreBlockSpin來更改)沒有成功獲得鎖,就應當使用傳統的方式去掛起線程了。
自旋鎖在JDK1.4.2中引入,使用-XX:+UseSpinning來開啟。JDK6中已經變為默認開啟,并且引入了自適應的自旋鎖。自適應意味著自旋的時間不在固定了,而是由前一次在同一個鎖上的自旋時間及鎖的擁有者的狀態來決定。
自旋是在輕量級鎖中使用的,在重量級鎖中,線程不使用自旋。
如果在同一個鎖對象上,自旋等待剛剛成功獲得過鎖,并且持有鎖的線程正在運行中,那么虛擬機就會認為這次自旋也是很有可能再次成功,進而它將允許自旋等待持續相對更長的時間,比如100次循環。另外,如果對于某個鎖,自旋很少成功獲得過,那在以后要獲取這個鎖時將可能省略掉自旋過程,以避免浪費處理器資源。
偏向鎖、輕量級鎖和重量級鎖
偏向鎖是JDK6中引入的一項鎖優化,它的目的是消除數據在無競爭情況下的同步原語,進一步提高程序的運行性能。
大多數情況下,鎖不僅不存在多線程競爭,而且總是由同一線程多次獲得,為了讓線程獲得鎖的代價更低而引入了偏向鎖。
偏向鎖會偏向于第一個獲得它的線程,如果在接下來的執行過程中,該鎖沒有被其他的線程獲取,則持有偏向鎖的線程將永遠不需要同步。
當鎖對象第一次被線程獲取的時候,線程使用CAS操作把這個鎖的線程ID記錄再對象Mark Word之中,同時置偏向標志位1。以后該線程在進入和退出同步塊時不需要進行CAS操作來加鎖和解鎖,只需要簡單地測試一下對象頭的Mark Word里是否存儲著指向當前線程的偏向鎖。如果測試成功,表示線程已經獲得了鎖。
如果線程使用CAS操作時失敗則表示該鎖對象上存在競爭并且這個時候另外一個線程獲得偏向鎖的所有權。當到達全局安全點時獲得偏向鎖的線程被掛起,膨脹為輕量級鎖,同時被撤銷偏向鎖的線程繼續往下執行同步代碼。
當有另外一個線程去嘗試獲取這個鎖時,偏向模式就宣告結束。
線程在執行同步塊之前,JVM會先在當前線程的棧幀中創建用于存儲鎖記錄(Lock Record)的空間,并將對象頭中的Mard Word復制到鎖記錄中,官方稱為Displaced Mark Word。然后線程嘗試使用CAS將對象頭中的Mark Word替換為指向鎖記錄的指針。如果成功,當前線程獲得鎖,如果失敗,表示其他線程競爭鎖,當前線程便嘗試使用自旋來獲取鎖。如果自旋失敗則鎖會膨脹成重量級鎖。如果自旋成功則依然處于輕量級鎖的狀態。
輕量級鎖提升程序同步性能的依據是:對于絕大部分的鎖,在整個同步周期內都是不存在競爭的(區別于偏向鎖)。這是一個經驗數據。如果沒有競爭,輕量級鎖使用CAS操作避免了使用互斥量的開銷,但如果存在鎖競爭,除了互斥量的開銷外,還額外發生了CAS操作,因此在有競爭的情況下,輕量級鎖比傳統的重量級鎖更慢。
整個synchronized鎖流程如下:
1. 檢測Mark Word里面是不是當前線程的ID,如果是,表示當前線程處于偏向鎖
2. 如果不是,則使用CAS將當前線程的ID替換Mard Word,如果成功則表示當前線程獲得偏向鎖,置偏向標志位1
3. 如果失敗,則說明發生競爭,撤銷偏向鎖,進而升級為輕量級鎖。
4. 當前線程使用CAS將對象頭的Mark Word替換為鎖記錄指針,如果成功,當前線程獲得鎖
5. 如果失敗,表示其他線程競爭鎖,當前線程便嘗試使用自旋來獲取鎖。
6. 如果自旋成功則依然處于輕量級狀態。
7. 如果自旋失敗,則升級為重量級鎖。
分段鎖
要降低鎖的競爭程度,其中有一種方式是:減少鎖的持有時間、縮小鎖的范圍、減少鎖的粒度。
這種技術可以采用多個相互獨立的鎖來保護共享資源來實現,這就是分段鎖。然而這會提高程序的復雜度,而且使用的鎖越多,發生死鎖的風險也就越高。但是要做全局的統計功能時還是需要對共享資源進行全局加鎖。
ConcurrentHashMap中采用了分段鎖。
悲觀鎖和樂觀鎖
悲觀鎖:假定會發生并發沖突,屏蔽一切可能違反數據完整性的操作,簡單地說是讀寫都加鎖。
樂觀鎖:假定不會發生并發沖突,只在提交操作時檢測是否違反數據完整性。(寫時加鎖,使用版本號或者時間戳來配合實現,如CAS).
死鎖
死鎖是指兩個或兩個以上的進程在執行過程中,因爭奪資源而造成的一種互相等待的現象,若無外力作用,他們都將無法推進下去。這是一個嚴重的問題,因為死鎖會讓你的程序掛起無法完成任務,死鎖的發生必須滿足一下4個條件:
- 互斥條件:一個資源每次只能被一個進程使用。
- 請求與保持條件:一個進程因請求資源而阻塞時,對已獲得的資源保持不放。
- 不剝奪條件:進程已獲得的資源,在未使用完之前,不能強行剝奪。
- 循環等待條件:若干進程之間形成一種頭尾相接的循環等待資源關系。
避免死鎖最簡單的方法就是破壞循環等待條件。
活鎖
LiveLock是一種形式活躍性問題,該問題盡管不會阻塞線程,但也不能繼續執行,因為線程將不斷重復執行相同的操作,而且總會失敗。當多個相互協作的線程都對彼此進行響應從而修改各自的狀態,并使得任何一個線程都無法繼續執行時,就發生了活鎖。這就像兩個過于禮貌的人在半路上面對面地相遇:他們彼此都給對方讓路,然而又在另一條路上相遇,就這樣反復里避讓下去,導致誰也過不去。
讀寫鎖、共享鎖和排它鎖、互斥鎖
略。