《Java并發(fā)編程之美》讀書筆記
鎖的概述
樂觀鎖與悲觀鎖
樂觀鎖和悲觀鎖是數(shù)據(jù)庫中引入的名字,但是在并發(fā)包里面也引入了類似的思想。
悲觀鎖是指對數(shù)據(jù)被外界修改持保守態(tài)度,認為數(shù)據(jù)很容易就被其他線程修改,所以在處理數(shù)據(jù)之前會先對數(shù)據(jù)進行加鎖。在整個數(shù)據(jù)處理的過程中,都使數(shù)據(jù)處于鎖定狀態(tài),悲觀鎖的實現(xiàn)往往依靠數(shù)據(jù)庫提供的鎖機制,即在數(shù)據(jù)庫中,在對數(shù)據(jù)記錄進行操作前給記錄增加排它鎖,如果獲取失敗,則說明數(shù)據(jù)正在被其他線程鎖修改,當(dāng)前線程則等待或者拋出異常。如果獲取鎖成功,則對記錄進行操作,然后提交事務(wù)后釋放排它鎖。
public int updateEntry(long id){
//使用悲觀鎖獲取指定記錄
EntryObject entry=query("select * from table1 where id=#{id} for update",id);
//修改記錄內(nèi)容,根據(jù)計算修改entry記錄的屬性
String name=generatorName(entry);
entry.setName(name);
//update操作
int count=update("update table1 set name=#{name},age=#{age} where id=#{id},entry");
return count;
}
對于如上代碼,假設(shè)updateEntry,query,update方法都采用了事務(wù)切面的方法,并且事務(wù)的傳播性被設(shè)置為了required。執(zhí)行updateEntry方法時如果上層的調(diào)用方法里面沒有開啟事務(wù),則會即使開啟一個事務(wù),然后執(zhí)行代碼1.代碼1調(diào)用了query方法,其根據(jù)指定id從數(shù)據(jù)可里面查詢出一個記錄。由于事務(wù)的傳播特性為required,所以只能夠query時,沒有開啟新的事務(wù),而是假如了updateEntry開啟的事務(wù),也就是在updateEntry方法執(zhí)行完畢提交事務(wù)時,query方法才會被提交,就是說記錄的鎖定會持續(xù)到updateEntry執(zhí)行結(jié)束。
接下來對獲取的記錄進行修改,再把修改的內(nèi)容寫回數(shù)據(jù)庫,同樣代碼的update方法沒有開啟新的事務(wù),而是假如了updateEntry的事務(wù),也就是updateEntry,query,update方法共用的同一個事務(wù)。
當(dāng)多個線程同時調(diào)用updateEntry方法的時候,并且傳遞的是同一個id時,只有一個線程執(zhí)行代碼會成功,其他線程則會被阻塞,這是因為在同一時間只有一個線程能獲取到對應(yīng)記錄的鎖,在獲取鎖的線程釋放鎖之前(updateEntry執(zhí)行完畢,提交事務(wù)之前)其他線程必須等待,也就是在同一個線只有一個線程可以對該記錄進行修改。
樂觀鎖
樂觀鎖相對于悲觀鎖來說,他認為數(shù)據(jù)在一般情況下不會造成沖突,所以在訪問記錄前不會加排它鎖,而是在進行數(shù)據(jù)提交更新時,才會正式對數(shù)據(jù)沖突與否進行檢測。具體來說,根據(jù)update返回的行數(shù)讓用戶決定如何去做。
public int updateEntry(long id){
//使用樂觀鎖獲取指定記錄
EntryObject entry=query("select * from table1 where id=#{id} for update",id);
//修改記錄內(nèi)容,version字段不能被修改
String name=generatorName(entry);
entry.setName(name);
//update操作
int count=update("update table1 set name=#{name},age=#{age} ,version=${version}+1 where id=#{id} and version=#{version},entry");
return count;
}
在如上的代碼中,當(dāng)多個線程調(diào)用updateEntry方法并且傳遞相同的id時,多個線程可以同時執(zhí)行代碼獲取id對應(yīng)的記錄并把記錄放入線程本地棧里面,然后可以同時執(zhí)行代碼對自己棧上的記錄進行修改,多個線程修改后各自的entry'里面的屬性都不一樣了。然后多個線程同時執(zhí)行代碼,大媽里面的update預(yù)計中的where條件加入了version=#{version},并且set語句里面多了version=${version}+1表達式,這個表達式的意思就是,如果數(shù)據(jù)庫里面id=#{id} and version=#{version}的記錄存在,就更新version的值為原來的值加上1,這有點CAS的意思。
假設(shè)多個線程同時執(zhí)行updateEntry并傳遞相同的id,那么他們執(zhí)行代碼時獲取的Entry時同一個,獲取Entry里面的version都是相同的,當(dāng)多個線程同時執(zhí)行update,由于update語句本身是原子性的,假如線程A執(zhí)行update成功了,那么這時候id對應(yīng)的記錄里面version的值加上了1,其他線程執(zhí)行更新時發(fā)現(xiàn)數(shù)據(jù)庫里面已經(jīng)沒有了version=0的語句,所以會返回影響的行號0,0在業(yè)務(wù)邏輯上就知道沒有更新成功,那么接下來有兩個做法,一是什么都不做,而是選擇重試。
public boolean updateEntry(long id){
boolean result=false;
int retryNum=5;
while(retryNum>0){
//使用樂觀鎖獲取指定記錄
EntryObject entry=query("select * from table1 where id=#{id} for update",id);
//修改記錄內(nèi)容,version字段不能被修改
String name=generatorName(entry);
entry.setName(name);
//update操作
int count=update("update table1 set name=#{name},age=#{age} ,version=${version}+1 where id=#{id} and version=#{version},entry");
if(count==1){
result=true;
break;
}
retryNum--;
}
return reslut;
}
如上的代碼retryNum設(shè)置更新失敗后的重試次數(shù),如果update操作返回0則說明記錄已經(jīng)被修改了,則循環(huán)一下,重新通過代碼獲取新的數(shù)據(jù),再嘗試更新,這種類似CAS的自旋操作。
樂觀鎖并不會使用數(shù)據(jù)庫提供的鎖機制,一般在表中添加version字段或者使用業(yè)務(wù)狀態(tài)來實現(xiàn)。樂觀鎖直到提交才鎖定,所以不會產(chǎn)生任何死鎖。
公平鎖和非公平鎖
根據(jù)線程獲取鎖的槍戰(zhàn)機制,鎖可以分為公平鎖和非公平鎖,公平鎖表示線程獲取到鎖的順序是按照線程請求鎖的時間早晚來決定的,也就是最早請求鎖的線程將最早獲取到鎖,而非公品鎖則在運行時闖入,也就是先來不一定先得。
ReentrantLock提供了公平鎖和非公平鎖的實現(xiàn)
- 公平鎖:
ReentrantLock pairLock=new ReentrantLock(true); - 非公平鎖:
ReentrantLock unpairLock=new ReentrantLock(false);如果構(gòu)造函數(shù)不傳遞參數(shù),則默認是非公平鎖。
例如,假設(shè)線程A已經(jīng)持有了鎖,這時候線程B請求該鎖則會被掛起。當(dāng)線程A釋放鎖之后,假如當(dāng)前有線程C也需要獲取該鎖如果采用非公平鎖的方式,根據(jù)線程調(diào)度策略,線程B和線程C兩則之一可能獲取鎖,這時候不需要任何其他的干涉,而如果使用公平鎖則需要把C掛起,讓B獲取當(dāng)前鎖。
在沒有公平性需求的情況下盡量使用非公平鎖,因為公平鎖會帶了性能開銷
獨占鎖與共享鎖
根據(jù)所只能被單個線程持有還是能被多個線程持有,鎖可以分為獨占鎖和共享鎖。獨占鎖可以保證任何時候只有一個線程能或得該鎖,ReentrantLock就是以獨占鎖的形式實現(xiàn)的,共享鎖則可以被多個線程所持有,例如ReadWriteLock讀寫鎖,它允許一個資源可以被多個線程同時進行讀操作。
獨占鎖是一種悲觀鎖,由于每次訪問資源先加上互斥所,這限制了并發(fā)性,因為讀操作并不會影響數(shù)據(jù)的一致性,而獨占鎖只允許在同一時間由一個線程讀取數(shù)據(jù),其他線程必須等待當(dāng)前線程釋放鎖才能進行讀取。
共享鎖則是一種樂觀鎖,它放寬了加鎖的條件,允許多個線程同時進行讀操作。
可重入鎖
當(dāng)一個線程要獲取一個被其他線程持有的獨占鎖時,這個線程會被阻塞,那么當(dāng)一個線程再次獲取它自己已經(jīng)獲取的鎖時會不會被阻塞呢?如果不被阻塞,那這個鎖就是可重入鎖,也就是獲取到了該鎖之后,可以有限次數(shù)的進入該鎖鎖住的代碼。
public class Hello {
public static synchronized void helloA(){
System.out.println("hello");
}
public static synchronized void helloB(){
System.out.println("hello b");
helloA();
}
public static void main(String[] args) {
Hello.helloB();
}
}
在如上的代碼中,調(diào)用helloB方法前會獲取內(nèi)置鎖,然后打印輸出,之后調(diào)用hellA的方法,在調(diào)用之前會先回去內(nèi)置鎖,如果鎖不是可重入的,那么調(diào)用線程則一直會被阻塞,實際上synchronized內(nèi)部鎖時可重入鎖。
可重入鎖的原理:在鎖的內(nèi)部維護一個線程標識,用來標示這個鎖目前被哪個線程鎖占用,然后關(guān)聯(lián)一個計數(shù)器。一開始計數(shù)器的值為0,說明該鎖沒有被任何線程占用,當(dāng)有一個線程獲取到該鎖的時候計數(shù)器的值就變?yōu)?,這時候其他線程再來獲取該鎖時防線鎖的所有者不是自己所以會被阻塞掛起。
但是當(dāng)獲取該鎖的線程再次獲取鎖時發(fā)現(xiàn)鎖擁有者還是自己,就把計數(shù)器的值加上1,當(dāng)釋放鎖的時候計數(shù)器的值-1;當(dāng)計數(shù)器的值為0時,鎖里面的線程標示被重置為null,這時候被阻塞的線程會被喚醒來競爭獲取該鎖。
自旋鎖
由于java中的線程和操作系統(tǒng)中的線程是一一對應(yīng)的,所以當(dāng)一個線程在獲取鎖(如獨占鎖)失敗的時候,會被切換到內(nèi)核狀態(tài)而被掛起。當(dāng)該線程獲取到鎖時又需要將其切換到內(nèi)核狀態(tài)而喚醒該線程。而從用戶態(tài)切換到內(nèi)核狀態(tài)的開銷是特別大的,在一定程度上會影響并發(fā)性能。自旋鎖則是,當(dāng)前線程在獲取鎖時,如果發(fā)現(xiàn)鎖已經(jīng)被其他線程所占有,它不會馬上阻塞自己,在不放棄CPU使用權(quán)的情況下,多次嘗試獲?。J次數(shù)時10,可以使用-XX:PreBlockSpinsh參數(shù)設(shè)置該值),很有可能在后面幾次嘗試中其他線程已經(jīng)釋放了該鎖。如果嘗試了指定次數(shù)之后其他線程仍然還沒有釋放該鎖則當(dāng)前線程才會被阻塞掛起。由此看來自旋鎖是使用CPU時間來換取線程阻塞和調(diào)度的開銷,但是很有可能這些CPU時間白白浪費了。
參考資料:
《Java并發(fā)編程之美》