Java Review - 并发编程_锁的分类
文章目錄
- 樂觀鎖與悲觀鎖
- 公平鎖與非公平鎖
- 獨占鎖與共享鎖
- 可重入鎖
- 自旋鎖
樂觀鎖與悲觀鎖
樂觀鎖和悲觀鎖是在數據庫中引入的名詞,但是在并發包鎖里面也引入了類似的思想。
悲觀鎖指對數據被外界修改持保守態度,認為數據很容易就會被其他線程修改,所以在數據被處理前先對數據進行加鎖,并在整個數據處理過程中,使數據處于鎖定狀態。
悲觀鎖的實現往往依靠數據庫提供的鎖機制,即在數據庫中,在對數據記錄操作前給記錄加排它鎖。
- 如果獲取鎖失敗,則說明數據正在被其他線程修改,當前線程則等待或者拋出異常。
- 如果獲取鎖成功,則對記錄進行操作,然后提交事務后釋放排它鎖。
來看個例子
public int updateentry(long id) {//(1)使用悲觀鎖獲取指定記錄Entryobiect entry = query("select * from tablel where id=#(id) for update", id);//(2)修改記錄內容,根據計算修改 entry記錄的屬性String name=generatorname(entry);entry.setname(name);//(3) update操作int count= update("update tablel set name=#(name], age=#fage] where id =#{id}", entry);return count;}假設updateEntry、query、update方法都使用了事務切面的方法,并且事務傳播性被設置為required
當多個線程同時調用updateEntry方法,并且傳遞的是同一個id時,只有一個線程執行代碼(1)會成功,其他線程則會被阻塞,這是因為在同一時間只有一個線程可以獲取對應記錄的鎖,在獲取鎖的線程釋放鎖前(updateEntry執行完畢,提交事務前),其他線程必須等待,也就是在同一時間只有一個線程可以對該記錄進行修改。
樂觀鎖是相對悲觀鎖來說的,它認為數據在一般情況下不會造成沖突,所以在訪問記錄前不會加排它鎖,而是在進行數據提交更新時,才會正式對數據沖突與否進行檢測。具體來說,根據 update 返回的行數讓用戶決定如何去做。
將上面的例子改為使用樂觀鎖的代碼如下。
public boolean updateentry(long id){boolean result = false;int retrynum = 5;while(retrynum >0){//(1.1)使用樂觀鎖獲取指定記錄Entryobject entry =query("select *from tablel where id =#[id)",id);//(2.1)修改記錄內容, version字段不能被修改String name =generatorname(entry);entry. setname(name);//(3.1) update操作int count = update("update tablel set name=#(name), age=#(age), version=${version}+1 where id =#(id) and version=#(version)", entry);if (count == 1){result = true ;break;}retrynum--;}return result;}如上代碼使用retryNum設置更新失敗后的重試次數,如果代碼(3.1)執行后返回0,則說明代碼(1.1)獲取的記錄已經被修改了,則循環一次,重新通過代碼(1.1)獲取最新的數據,然后再次執行代碼(3.1)嘗試更新。這類似CAS的自旋操作,只是這里沒有使用死循環,而是指定了嘗試次數。
樂觀鎖并不會使用數據庫提供的鎖機制,一般在表中添加 version 字段或者使用業務狀態來實現。樂觀鎖直到提交時才鎖定,所以不會產生任何死鎖。
公平鎖與非公平鎖
根據線程獲取鎖的搶占機制,鎖可以分為公平鎖和非公平鎖
-
公平鎖表示線程獲取鎖的順序是按照線程請求鎖的時間早晚來決定的,也就是最早請求鎖的線程將最早獲取到鎖。
-
非公平鎖則在運行時闖入,也就是先來不一定先得。
ReentrantLock 提供了公平和非公平鎖的實現。
-
公平鎖:ReentrantLock pairLock = new ReentrantLock(true)。
-
非公平鎖:ReentrantLock pairLock = new ReentrantLock(false)。如果構造函數不傳遞參數,則默認是非公平鎖。
假設線程 A 已經持有了鎖,這時候線程 B 請求該鎖其將會被掛起。當線程 A釋放鎖后,假如當前有線程 C 也需要獲取該鎖,如果采用非公平鎖方式,則根據線程調度策略,線程 B 和 線程C 兩者之一可能獲取鎖,這時候不需要任何其他干涉,而如果使用公平鎖則需要把 C 掛起,讓 B 獲取當前鎖。
在沒有公平性需求的前提下盡量使用非公平鎖,因為公平鎖會帶來性能開銷。
獨占鎖與共享鎖
根據鎖只能被單個線程持有還是能被多個線程共同持有,鎖可以分為獨占鎖和共享鎖。
-
獨占鎖保證任何時候都只有一個線程能得到鎖
獨占鎖是一種悲觀鎖,由于每次訪問資源都先加上互斥鎖,這限制了并發性,因為讀操作并不會影響數據的一致性,而獨占鎖只允許在同一時間由一個線程讀取數據,其他線程必須等待當前線程釋放鎖才能進行讀取。
-
共享鎖則是一種樂觀鎖,它放寬了加鎖的條件,允許多個線程同時進行讀操作。
ReentrantLock 就是以獨占方式實現的。
共享鎖則可以同時由多個線程持有 ReadWriteLock 讀寫鎖,它允許一個資源可以被多線程同時進行讀操作。
可重入鎖
當一個線程要獲取一個被其他線程持有的獨占鎖時,該線程會被阻塞,那么當一個線程再次獲取它自己已經獲取的鎖時是否會被阻塞呢?如果不被阻塞,那么我們說該鎖是可重入的,也就是只要該線程獲取了該鎖,那么可以無限次數(嚴格來說是有限次數)地進入被該鎖鎖住的代碼。
來個Demo, 看看在什么情況下會使用可重入鎖
/*** @author 小工匠* @version 1.0* @description: TODO* @date 2021/11/27 10:43* @mark: show me the code , change the world*/ public class KeCongRuLockDemo {// synchronized public synchronized void lockA(){System.out.println("lockA");}// synchronized public synchronized void lockB(){System.out.println("lockB , then call lockA");// 調用加鎖的A方法lockA();}public static void main(String[] args) {KeCongRuLockDemo lockDemo = new KeCongRuLockDemo();lockDemo.lockB();} }在如上代碼中,調用 lockB方法前會先獲取內置鎖,然后打印輸出。之后調用 lockA方法,在調用前會先去獲取內置鎖,如果內置鎖不是可重入的,那么調用線程將會一直被阻塞。
實際上, synchronized 內部鎖是可重入鎖。
可重入鎖的原理是在鎖內部維護一個線程標示,用來標示該鎖目前被哪個線程占用,然后關聯一個計數器。一開始計數器值為0,說明該鎖沒有被任何線程占用。當一個線程獲取了該鎖時,計數器的值會變成1,這時其他線程再來獲取該鎖時會發現鎖的所有者不是自己而被阻塞掛起。
但是當獲取了該鎖的線程再次獲取鎖時發現鎖擁有者是自己,就會把計數器值加+1,當釋放鎖后計數器值-1。當計數器值為0時,鎖里面的線程標示被重置為 null,這時候被阻塞的線程會被喚醒來競爭獲取該鎖。
可重入鎖兩個關鍵字: 線程標示 + 計數器
自旋鎖
由于Java中的線程是與操作系統中的線程一一對應的,所以當一個線程在獲取鎖(比如獨占鎖)失敗后,會被切換到內核狀態而被掛起。
當該線程獲取到鎖時又需要將其切換到內核狀態而喚醒該線程。而從用戶狀態切換到內核狀態的開銷是比較大的,在一定程度上會影響并發性能。
自旋鎖是當前線程在獲取鎖時,如果發現鎖已經被其他線程占有,它不馬上阻塞自己,在不放棄CPU使用權的情況下,多次嘗試獲取(默認次數是10,可以使用-XX:PreBlockSpinsh參數設置該值),很有可能在后面幾次嘗試中其他線程已經釋放了鎖。如果嘗試指定的次數后仍沒有獲取到鎖則當前線程才會被阻塞掛起。
由此看來自旋鎖是使用CPU時間換取線程阻塞與調度的開銷,但是很有可能這些CPU時間白白浪費了.
總結
以上是生活随笔為你收集整理的Java Review - 并发编程_锁的分类的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Java Review - 并发编程_前
- 下一篇: Java Review - 并发编程_U