樂觀鎖 & 悲觀鎖
樂觀鎖和悲觀鎖是在數(shù)據(jù)庫中引入的名詞斋陪,但是在 Java 并發(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ù)正在被其他線程修改,當前線程則等待或者拋出異常肥矢。如果獲取鎖成功端衰,則對記錄進行操作,然后提交事務后釋放排它鎖甘改。
public int updateEntry(long id) l
// 使用悲觀鎖獲取指定記錄
EntryObject entry = query("select * from table1 where id=#{id} for update", id) ;
// 修改名稱
entry.setName (“new name”) ;
// 執(zhí)行更新操作
int count = update("update table1 set name=#{name}, age=#{age} where id=#{id}",entry) ;
return count;
}
當多個線程同時調(diào)用 updateEntry 方法,并且傳遞的是同一個id時,只有一個線程執(zhí)行代碼 query 方法會成功旅东,其他線程則會被阻塞,這是因為在同一時間只有一個線程可以獲取對應記錄的鎖,在獲取鎖的線程釋放鎖前 ( update Entry執(zhí)行完畢,提交事務之前 )十艾,其他線程必須等待,也就是在同一時間只有一個線程可以對該記錄進行修改抵代。
樂觀鎖
樂觀鎖 是相對悲觀鎖來說的,它認為數(shù)據(jù)在一般情況下不會造成沖突忘嫉,所以在訪問記錄前不會加排它鎖荤牍,而是在進行數(shù)據(jù)提交更新時,才會正式對數(shù)據(jù)沖突與否進行檢測庆冕。具體來說康吵,根據(jù)update返回的行數(shù)讓用戶決定如何去做。將上面的例子改為使用樂觀鎖的代碼如下访递。
public int updateEntry(long id) {
//(1)使用樂觀鎖獲取指定記錄
EntryObject entry = query("select * from table1 where id = #{id}",id) ;
//(2)修改記錄內(nèi)容晦嵌,version 字段不能被修改
entry.setName(“name”);
// (3) update操作
int count = update("update table1 set name=#{name} , age=#{age}, version=${version}+1 where id =#{id} and version=#{version}",entry);
return count;
}
樂觀鎖并不會使用數(shù)據(jù)庫提供的鎖機制,一般在表中添加version字段或者使用業(yè)務狀態(tài)來實現(xiàn)拷姿。樂觀鎖直到提交時才鎖定惭载,所以不會產(chǎn)生任何死鎖。
公平鎖 & 非公平鎖
根據(jù)線程獲取鎖的搶占機制响巢,鎖可以分為公平鎖和非公平鎖描滔,公平鎖表示線程獲取鎖的順序是按照線程請求鎖的時間先后來決定的,也就是最先請求鎖的線程將最先獲取到鎖抵乓。而非公平鎖則在運行時闖入伴挚,也就是先來不一定先得。
例如灾炭,假設線程A已經(jīng)持有了鎖,這時候線程B請求該鎖其將會被掛起颅眶。當線程A釋放鎖后蜈出,假如當前有線程C也需要獲取該鎖,如果采用非公平鎖方式涛酗,則根據(jù)線程調(diào)度策略铡原,線程B和線程C兩者之一可能獲取鎖偷厦,這時候不需要任何其他干涉,而如果使用公平鎖則需要把C掛起燕刻,讓B獲取當前鎖只泼。
在沒有公平性需求的前提下盡量使用非公平鎖,因為公平鎖會帶來性能開銷
獨占鎖 & 共享鎖
根據(jù)鎖只能被單個線程持有還是能被多個線程共同持有,鎖可以分為獨占鎖和共享鎖卵洗。
獨占鎖保證任何時候都只有一個線程能得到鎖请唱,ReentrantLock 就是以獨占方式實現(xiàn)的。共享鎖則可以同時由多個線程持有过蹂,例如 ReadWriteLock 讀寫鎖十绑,它允許一個資源可以被多線程同時進行讀操作。
獨占鎖是一種悲觀鎖酷勺,由于每次訪問資源都先加上互斥鎖本橙,這限制了并發(fā)性,因為讀操作并不會影響數(shù)據(jù)的一致性脆诉,而獨占鎖只允許在同一時間由一個線程讀取數(shù)據(jù)甚亭,其他線程必須等待當前線程釋放鎖才能進行讀取。
共享鎖則是一種樂觀鎖击胜,它放寬了加鎖的條件亏狰,允許多個線程同時進行讀操作。
可重入鎖
當一個線程要獲取一個被其他線程持有的獨占鎖時潜的,該線程會被阻塞骚揍,那么當一個線程再次獲取它自己已經(jīng)獲取的鎖時是否會被阻塞呢?如果不被阻塞,那么我們說該鎖是可重入的啰挪,也就是只要該線程獲取了該鎖信不,那么可以無限次數(shù)(嚴格來說是有限次數(shù))地進入被該鎖鎖住的代碼。
synchronized 內(nèi)部鎖是可重入鎖亡呵〕榛睿可重入鎖的原理是在鎖內(nèi)部維護一個線程
標示,用來標示該鎖目前被哪個線程占用锰什,然后關聯(lián)一個計數(shù)器下硕。一開始計數(shù)器值為 0,說明該鎖沒有被任何線程占用汁胆。當一個線程獲取了該鎖時,計數(shù)器的值會變成1,這時其他線程再來獲取該鎖時會發(fā)現(xiàn)鎖的所有者不是自己而被阻塞掛起梭姓。
但是當獲取了該鎖的線程再次獲取鎖時發(fā)現(xiàn)鎖擁有者是自己,就會把計數(shù)器值加+1嫩码,當釋放鎖后計數(shù)器值-1誉尖。當計數(shù)器值為0時,鎖里面的線程標示被重置為null铸题,這時候被阻塞的線程會被喚醒來競爭獲取該鎖铡恕。
自旋鎖
由于Java中的線程是與操作系統(tǒng)中的線程一一對應的琢感,所以當一個線程在獲取鎖(比如獨占鎖)失敗后,會被切換到內(nèi)核狀態(tài)而被掛起探熔。當該線程獲取到鎖時又需要將其切換到內(nèi)核狀態(tài)而喚醒該線程驹针。而從用戶狀態(tài)切換到內(nèi)核狀態(tài)的開銷是比較大的,在一定程度上會影響并發(fā)性能诀艰。自旋鎖則是柬甥,當前線程在獲取鎖時,如果發(fā)現(xiàn)鎖已經(jīng)被其他線程占有涡驮,它不馬上阻塞自己暗甥,在不放棄CPU使用權的情況下,多次嘗試獲取(默認次數(shù)是10捉捅,可以使用 -XX:PreBlockSpinsh 參數(shù)設置該值)撤防,很有可能在后面幾次嘗試中其他線程已經(jīng)釋放了鎖。如果嘗試指定的次數(shù)后仍沒有獲取到鎖則當前線程才會被阻塞掛起棒口。由此看來自旋鎖是使用CPU時間換取線程阻塞與調(diào)度的開銷寄月,但是很有可能這些CPU時間白白浪費了。
Lock 接口
在 Lock 接口出現(xiàn)之前无牵,Java 程序是靠 synchronized 關鍵字實現(xiàn)鎖功能的漾肮,而 Java Se5 之后,并發(fā)包中新增了 Lock 接口(以及相關實現(xiàn)類)用來實現(xiàn)鎖功能茎毁,它提供了與 synchronized 關鍵字類似的同步功能,只是在使用時需要顯式地獲取和釋放鎖克懊。雖然它缺少了(通過 synchronized 塊或者方法所提供的)隱式獲取釋放鎖的便捷性,但是卻擁有了鎖獲取與釋放的可操作性、可中斷的獲取鎖以及超時獲取鎖等多種 synchronized 關鍵字所不具備的同步
特性七蜘。
使用 synchronized 關鍵字將會隱式地獲取鎖谭溉,但是它將鎖的獲取和釋放固化了,也就是先獲取再釋放橡卤。當然,這種方式簡化了同步的管理扮念,可是擴展性沒有顯示的鎖獲取和釋放來的好。Lock 的使用其實也很簡單碧库,如下代碼
Lock lock = new ReentrantLock();
lock.lock();
try{
}finally{
lock.unlock();
}
在 finally 塊中釋放鎖柜与,目的是保證在獲取到鎖之后,最終能夠被釋放嵌灰。
Lock 接口方法
void lock()喂窟;
獲取鎖,調(diào)用該方法當前線程將會獲取鎖,當鎖獲得后,從該方法返回void lockInterruptibly() throws InterruptedException恋拍;
可中斷地獲取鎖咐鹤,和 lock 方法的不同之處在于該方法會響應中斷霸旗,即在鎖的獲取中可以中斷當前線程boolean tryLock()
嘗試非阻塞的獲取鎖,調(diào)用該方法后立刻返回,如果能夠獲取則返回 true秕脓,否則返回 false-
boolean tryLock(long time, TimeUnit unit) throws InterruptedException柒瓣;
超時的獲取鎖,當前線程在以下3種情況下會返回:- 當前線程在超時時間內(nèi)獲得了鎖
- 當前線程在超時間內(nèi)被中斷
- 超時時間結束,返回 false
void unlock
釋放鎖Condition newCondition();
獲取等待通知組件吠架,該組件和當前的鎖綁定芙贫,當前線程只有獲得了鎖,才能調(diào)用該組件的 wait 方法傍药,而調(diào)用后磺平,當前線程將釋放鎖