鎖分類介紹



本博客系列是學習並發編程過程中的記錄總結。由於文章比較多,寫的時間也比較散,所以我整理了個目錄貼(傳送門),方便查閱。

並發編程系列博客傳送門


樂觀鎖和悲觀鎖

鎖從宏觀上來分類,可以分為悲觀鎖與樂觀鎖。注意,這里說的的鎖可以是數據庫中的鎖,也可以是Java等開發語言中的鎖技術。悲觀鎖和樂觀鎖其實只是一類概念(對某類具體鎖的總稱),不是某種語言或是某個技術獨有的鎖技術。

樂觀鎖是一種樂觀思想,即認為讀多寫少,遇到並發寫的可能性低,每次去拿數據的時候都認為別人不會修改,所以不會上鎖,但是在更新的時候會判斷一下在此期間別人有沒有去更新這個數據,采取在寫時先讀出當前版本號,然后加鎖操作(比較跟上一次的版本號,如果一樣則更新),如果失敗則要重復讀-比較-寫的操作。java中的樂觀鎖基本都是通過CAS操作實現的,CAS是一種更新的原子操作,比較當前值跟傳入值是否一樣,一樣則更新,否則失敗。數據庫中的共享鎖也是一種樂觀鎖。

悲觀鎖是就是悲觀思想,即認為寫多,遇到並發寫的可能性高,每次去拿數據的時候都認為別人會修改,所以每次在讀寫數據的時候都會上鎖,這樣別人想讀寫這個數據就會block直到拿到鎖。java中典型的悲觀鎖就是Synchronized,AQS框架下的鎖則是先嘗試cas樂觀鎖去獲取鎖,獲取不到,才會轉換為悲觀鎖,如ReentrantLock。數據庫中的排他鎖也是一種悲觀鎖。

公平鎖與非公平鎖

根據線程獲取鎖的搶占機制,鎖可以分為公平鎖和非公平鎖,公平鎖表示線程獲取鎖的順序是按照線程請求鎖的時間早晚來決定的,也就是最早請求鎖的線程將最早獲取到鎖。而非公平鎖則在運行時闖入,也就是先來不一定先得。

ReentrantLock提供了公平和非公平鎖的實現。

  • 公平鎖:ReentrantLock pairLock = new ReentrantLock(true)。
  • 非公平鎖:ReentrantLock pairLock = new ReentrantLock(false)。如果構造函數不傳遞參數,則默認是非公平鎖。

例如,假設線程A已經持有了鎖,這時候線程B請求該鎖其將會被掛起。當線程A釋放鎖后,假如當前有線程C也需要獲取該鎖,如果采用非公平鎖方式,則根據線程調度策略,線程B和線程C兩者之一可能獲取鎖,這時候不需要任何其他干涉,而如果使用公平鎖則需要把C掛起,讓B獲取當前鎖。

在沒有公平性需求的前提下盡量使用非公平鎖,因為公平鎖會帶來性能開銷。

獨占鎖和共享鎖

根據鎖只能被單個線程持有還是能被多個線程共同持有,鎖可以分為獨占鎖和共享鎖。

獨占鎖保證任何時候都只有一個線程能得到鎖,ReentrantLock就是以獨占方式實現的。共享鎖則可以同時由多個線程持有,例如ReadWriteLock讀寫鎖,它允許一個資源可以被多線程同時進行讀操作。

獨占鎖是一種悲觀鎖,由於每次訪問資源都先加上互斥鎖,這限制了並發性,因為讀操作並不會影響數據的一致性,而獨占鎖只允許在同一時間由一個線程讀取數據,其他線程必須等待當前線程釋放鎖才能進行讀取。

共享鎖則是一種樂觀鎖,它放寬了加鎖的條件,允許多個線程同時進行讀操作。

可重入鎖

當一個線程要獲取一個被其他線程持有的獨占鎖時,該線程會被阻塞,那么當一個線程再次獲取它自己已經獲取的鎖時是否會被阻塞呢?如果不被阻塞,那么我們說該鎖是可重入的,也就是只要該線程獲取了該鎖,那么可以無限次數(嚴格來說是有限次數)地進入被該鎖鎖住的代碼。

public synchronized void helloA(){
...
}

public synchronized void helloB(){
    ...
    helloA();
}

在如上代碼中,調用helloB方法前會先獲取內置鎖,然后打印輸出。之后調用helloA方法,在調用前會先去獲取內置鎖,如果內置鎖不是可重入的,那么調用線程將會一直被阻塞。

實際上,synchronized內部鎖是可重入鎖。可重入鎖的原理是在鎖內部維護一個線程標示,用來標示該鎖目前被哪個線程占用,然后關聯一個計數器。一開始計數器值為0,說明該鎖沒有被任何線程占用。當一個線程獲取了該鎖時,計數器的值會變成1,這時其他線程再來獲取該鎖時會發現鎖的所有者不是自己而被阻塞掛起。

但是當獲取了該鎖的線程再次獲取鎖時發現鎖擁有者是自己,就會把計數器值加+1,當釋放鎖后計數器值-1。當計數器值為0時,鎖里面的線程標示被重置為null,這時候被阻塞的線程會被喚醒來競爭獲取該鎖。

自旋鎖

由於Java中的線程是與操作系統中的線程一一對應的,所以當一個線程在獲取鎖(比如獨占鎖)失敗后,會被切換到內核狀態而被掛起。當該線程獲取到鎖時又需要將其切換到內核狀態而喚醒該線程。而從用戶狀態切換到內核狀態的開銷是比較大的,在一定程度上會影響並發性能。自旋鎖則是,當前線程在獲取鎖時,如果發現鎖已經被其他線程占有,它不馬上阻塞自己,在不放棄CPU使用權的情況下,多次嘗試獲取(默認次數是10,可以使用-XX:PreBlockSpinsh參數設置該值),很有可能在后面幾次嘗試中其他線程已經釋放了鎖。如果嘗試指定的次數后仍沒有獲取到鎖則當前線程才會被阻塞掛起。由此看來自旋鎖是使用CPU時間換取線程阻塞與調度的開銷,但是很有可能這些CPU時間白白浪費了。

參考

  • 《Java並發編程之美》


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM