Java多線程中常見的鎖策略詳解
1. 悲觀鎖與樂觀鎖
悲觀鎖:為了保證原子性,因此把數(shù)據(jù)進(jìn)行上鎖,每一個不同的線程拿數(shù)據(jù)的時候都會參與鎖的競爭,其他線程想必須等待前者拿完數(shù)據(jù)解鎖后才能參與拿數(shù)據(jù)。
舉例,由于維修導(dǎo)致一層樓只剩下一間廁所。因此,線程1進(jìn)入廁所后,其他線程只能阻塞等待。
樂觀鎖:假設(shè)數(shù)據(jù)一般情況下不會產(chǎn)生并發(fā)沖突,所以在數(shù)據(jù)進(jìn)行提交更新的時候,才會正式對數(shù)據(jù)是否產(chǎn)生并發(fā)沖突進(jìn)行檢測,如果發(fā)現(xiàn)并發(fā)沖突了,則讓返回用戶錯誤的信息,讓用戶決定如何去做。
還是上述上廁所例子,線程1 給其他線返回一個信息,其他線程可根據(jù)信息選擇換樓層上廁所亦或是等待。
以上的上廁所例子,在樓棟廁所不充足情況下。線程還是盲目選擇這棟樓的廁所(使用悲觀鎖)就會導(dǎo)致阻塞消耗系統(tǒng)資源。在樓棟廁所充足的情況下,線程選擇了這棟樓的廁所(使用樂觀鎖)這樣就能很好的利用系統(tǒng)資源。
synchronized 初始情況下使用的是樂觀鎖,當(dāng)發(fā)現(xiàn)鎖競爭激烈時候就會自動轉(zhuǎn)換為悲觀鎖。就好比一個線程去某一棟樓上廁所,并不知道該樓棟是否廁所充足。充足就不阻塞等待,不充足就阻塞等待。
2. 讀寫鎖與互斥鎖
多線程中,線程作為讀取方不會產(chǎn)生線程安全問題,當(dāng)線程作為為寫入方和線程作為寫入方之間進(jìn)行交互和,線程作為寫入方和線程作為讀取方之間進(jìn)行交互,就會造成互斥。
線程對數(shù)據(jù)的訪問,主要存在三種情況:
- 線程只是對數(shù)據(jù)進(jìn)行讀操作,此時自然不會出現(xiàn)線程不安全問題。
- 多個線程對數(shù)據(jù)進(jìn)行寫操作,就會出現(xiàn)線程不安全問題。
- 一個線程對數(shù)據(jù)進(jìn)行讀操作,另個線程對數(shù)據(jù)進(jìn)行寫操作,也會出現(xiàn)線程不安全問題。
簡單的來說,線程的讀操作,就是線程對數(shù)據(jù)進(jìn)行訪問。線程的寫操作,就是線程對數(shù)據(jù)進(jìn)行修改。讀一下問題不大,但寫一下就難免會造成意外。因此,我們有了 讀寫鎖 這個概念。
讀寫鎖,就是把讀和寫這兩個操作分開來加鎖這樣就能避免互斥。Java 標(biāo)準(zhǔn)庫提供了一個 ReentrantReadWriteLock 類,實現(xiàn)了讀寫鎖。
ReentrantReadWriteLock.ReadLock 類表示一個讀鎖.。這個對象提供了 lock / unlock 方法進(jìn)行加鎖解鎖。
ReentrantReadWriteLock.WriteLock 類表示一個寫鎖.。這個對象也提供了 lock / unlock 方法進(jìn)行加鎖解鎖。
- 讀加鎖與讀加鎖之間,不互斥
- 寫加鎖與寫加鎖之間,互斥
- 讀加鎖與寫加鎖之間,互斥
互斥,就會操作線程的掛起等待,一旦線程掛起等待了,就不知道什么時候能夠被喚醒了。因此,我們在編寫代碼的時候盡可能減少互斥。
讀寫鎖特別適用于“頻繁讀,不頻繁寫”的場景中。比如,學(xué)校的教務(wù)系統(tǒng):
假設(shè)計算機(jī)軟件專業(yè)的學(xué)生有 300 個同學(xué),這300個同學(xué)幾乎每天都要課程表為了防止課表更改,這樣的一個操作就是頻繁讀(訪問)。
有特殊情況,老師生病了或是怎樣,偶爾會調(diào)課到其他時間點。這樣的操作,就是不頻繁寫(修改)。
注意,synchronized 不是讀寫鎖。
3. 重量級鎖與輕量級鎖
在并發(fā)編程中,輕量級鎖和重量級鎖是兩種鎖的實現(xiàn)方法,主要用于解決多個線程同時訪問共享資源時的同步問題。
輕量級鎖通常用于鎖競爭不激烈的情況下,通過在線程內(nèi)部使用CAS操作來進(jìn)行加鎖和解鎖,這種方式不需要進(jìn)行線程的上下文切換,因此性能比重量級鎖更高。但是,如果鎖競爭激烈的話,輕量級鎖的性能優(yōu)勢就不明顯了。
重量級鎖通常用于鎖競爭激烈的情況下,通過將競爭鎖的線程掛起并切換到內(nèi)核態(tài)來進(jìn)行加鎖和解鎖。由于需要進(jìn)行線程的上下文切換,因此性能比輕量級鎖更低。但是,在鎖競爭激烈的情況下,重量級鎖的效果要比輕量級鎖好得多,因為它可以有效地避免鎖爭用問題,減少了線程的搶占和切換,從而提高了系統(tǒng)的效率和響應(yīng)速度。
synchronized 的輕量級鎖策略大概都是通過自旋鎖的方式實現(xiàn)的,重量級鎖則是掛起等待鎖。
4. 自旋鎖與掛起等待鎖
自旋鎖 VS 掛起等待鎖:
自旋鎖,當(dāng)線程之間進(jìn)行搶占鎖內(nèi)資源時候,線程1 已經(jīng)搶占到鎖,線程2 則會持續(xù)等待 線程1 鎖內(nèi)任務(wù)結(jié)束后再進(jìn)行搶占鎖資源,在這期間 線程2 持續(xù)處于阻塞等待狀態(tài)。
掛起等待鎖,當(dāng)掛起等待鎖遇到這種情況時,發(fā)現(xiàn)有線程已經(jīng)搶占到鎖了,則會放棄阻塞等待。直到鎖開放了,則再參與搶占鎖。
因此,自旋鎖有以下優(yōu)缺點:
- 優(yōu)點:時刻占用系統(tǒng)資源,不涉及線程阻塞和調(diào)度,一旦鎖被釋放了,參與鎖的競爭。
- 缺點:當(dāng)鎖內(nèi)任務(wù)比較復(fù)雜時,鎖被其他線程占有時間過長,那么就會持續(xù)消耗系統(tǒng)資源。
- 掛起等待鎖則相反
4.1 自旋鎖
自旋鎖,按照正常的邏輯,當(dāng)線程搶占鎖時進(jìn)入阻塞狀態(tài),過不了多久鎖就被釋放了。因此,自旋鎖就沒必要放棄 CPU 了,一直占用著 CPU 的內(nèi)存空間。
自旋鎖偽代碼:
while(槍鎖lock == 失敗) {}
如果獲取鎖失敗,立即再嘗試獲取鎖,無限循環(huán)下去直到獲取到鎖為止。第一次獲取鎖失敗,往后的獲取鎖操作會在極短的時間內(nèi)到來,一旦鎖被其他線程釋放,就能第一時間獲取到鎖。這就是輕量級鎖的體現(xiàn)(鎖的競爭還不太激烈,嘗試使用自旋方式加鎖)。
4.2 掛起等待鎖
當(dāng)線程獲取鎖失敗后,并不會進(jìn)行阻塞等待。而隨著系統(tǒng)的調(diào)度,不占用 CPU 。直到鎖開發(fā)后,再嘗試參與鎖競爭。這種情況就是掛起等待鎖,也是重量級鎖的體現(xiàn)(鎖的競爭太激烈了,線程跟隨系統(tǒng)的調(diào)度)。
舉例:
自旋鎖與掛起等待鎖的現(xiàn)實生活體現(xiàn):張三是一個普通的男生,如花是一個漂亮的女孩,在此張三作為線程,如花作為鎖。
張三開始追求如花,但是如花已經(jīng)有男朋友了。張三又是個死皮賴臉的人。每天堅持給女孩發(fā)信息,期待著某一天如花分手,能得到如花。此時張三就處于自旋鎖的狀態(tài)。
隨著競爭的激烈,又有許多人想要追求如花。張三開始動搖了,開始努力敲代碼、認(rèn)真學(xué)習(xí)不參與追如花的競爭了(隨著系統(tǒng)的調(diào)度做其他事去了)。如果某一天如花變?yōu)閱紊砹?,系統(tǒng)會通知張三如花單身了(鎖空閑了),張三就又開始參與競爭鎖。此時張三的狀態(tài)就是掛起等待鎖狀態(tài)。
5. 公平鎖與非公平鎖
公平鎖與非公平鎖講究四個字“公平競爭”,假設(shè)有三個線程搶占鎖資源,當(dāng)鎖被釋放后就會出現(xiàn)兩種情況:公平競爭鎖、非公平競爭鎖。
公平鎖:遵循先來后到的原則,線程1 進(jìn)入鎖,鎖釋放后。線程2 進(jìn)入鎖,鎖再釋放后。線程3 進(jìn)入鎖。整個過程是按照順序執(zhí)行的。
非公平鎖:由于線程之間搶占資源,導(dǎo)致鎖被無序的搶占。這樣 3 個線程都有機(jī)會優(yōu)先進(jìn)入鎖。整個過程會造成無序執(zhí)行。
通過上圖我們就能很好的理解,公平鎖與非公平鎖之間的差異。當(dāng)然,線程的調(diào)度是隨機(jī)的因此多個線程競爭鎖時可以隨意進(jìn)行搶占“手快有,手慢無”(非公平鎖)。要想實現(xiàn)公平鎖,我們可以使用一些特定的數(shù)據(jù)結(jié)構(gòu)來達(dá)到按順序使用鎖。
在實際開發(fā)中,公平鎖與非公平鎖沒有好壞之分,我們按照需求來進(jìn)行設(shè)置。注意,synchronized 屬于非公平鎖。
6. 可重入鎖與不可重入鎖
可重入鎖即允許一個線程多次獲取同一把鎖。
不可重入鎖是指一旦線程獲得了該鎖,此時再次請求獲取該鎖時,系統(tǒng)會將該線程掛起,直到該鎖被釋放為止。因此,不可重入鎖不能再同一線程中重復(fù)獲取。
可重入鎖是指當(dāng)一個線程獲得了該鎖之后,在該鎖還未釋放之前,可以再次獲取該鎖。這種鎖可以防止死鎖的發(fā)生,因為在獲取之后可以在方法中重新獲取該鎖,從而避免死鎖的發(fā)生。
Java 中的 synchronized 關(guān)鍵字是一種可重入鎖,而 ReentrantLock 是 Java 中常用的可重入鎖類,synchronized 不需要手動解鎖,而 ReentrantLock 需要手動解鎖。
需要注意的是,可重入鎖雖然提高了代碼的靈活性和可維護(hù)性,但同時也可能會帶來出現(xiàn)深度嵌套鎖的風(fēng)險,引發(fā)死鎖或性能下降等問題。因此,在使用可重入鎖時需要仔細(xì)設(shè)計和管理。
談?wù)勀銓ynchronized的演變過程的理解?
synchronized 既是悲觀鎖也是樂觀鎖,synchronized 即是輕量級鎖也是重量級鎖,synchronized 即是自旋鎖也是掛起等待鎖,synchronized 不是讀寫鎖,synchronized 是非公平鎖,synchronized是可重入鎖。
synchronized 的初始化的時候是一個樂觀鎖/輕量級鎖/自旋鎖,隨著synchronized的競爭激烈會升級為悲觀鎖/重量級鎖/掛起等待鎖,另外輕量級鎖是部分基于自旋鎖、重量級鎖是部分基于掛起等待鎖。
在鎖的策略中還會引申到“死鎖”的概念,在下篇博文中,我會介紹。大家也可以通過下方專欄中搜索多線程相關(guān)內(nèi)容。
到此這篇關(guān)于Java多線程中常見的鎖策略詳解的文章就介紹到這了,更多相關(guān)Java常見的鎖策略內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
spring 整合 mybatis 中數(shù)據(jù)源的幾種配置方式(總結(jié)篇)
因為spring 整合mybatis的過程中, 有好幾種整合方式,尤其是數(shù)據(jù)源那塊,經(jīng)??吹讲灰粯拥呐渲梅绞剑偢杏X有點亂,所以今天有空總結(jié)下,感興趣的朋友跟隨腳本之家小編一起學(xué)習(xí)吧2018-05-05java中int、double、char等變量的取值范圍詳析
這篇文章主要給大家介紹了關(guān)于java中int、double、char等變量取值范圍的相關(guān)資料,每個變量都給出了詳細(xì)的實例代碼,對大家學(xué)習(xí)或者使用java具有一定的參考學(xué)習(xí)價值,需要的朋友可以參考下2021-10-10Java讀寫txt文件時防止中文亂碼問題出現(xiàn)的方法介紹
這篇文章主要介紹了Java讀寫txt文件時防止中文亂碼問題出現(xiàn)的方法,同時需要注意系統(tǒng)默認(rèn)的文本保存編碼的設(shè)置,需要的朋友可以參考下2015-12-12java 中String和StringBuffer與StringBuilder的區(qū)別及使用方法
這篇文章主要介紹了java 中String和StringBuffer與StringBuilder的區(qū)別及使用方法的相關(guān)資料,在開發(fā)過程中經(jīng)常會用到String 這個類進(jìn)行操作,需要的朋友可以參考下2017-08-08Spring中的ThreadPoolTaskExecutor線程池使用詳解
這篇文章主要介紹了Spring中的ThreadPoolTaskExecutor線程池使用詳解,ThreadPoolTaskExecutor 是 Spring框架提供的一個線程池實現(xiàn),用于管理和執(zhí)行多線程任務(wù),它是TaskExecutor接口的實現(xiàn),提供了在 Spring 應(yīng)用程序中創(chuàng)建和配置線程池的便捷方式,需要的朋友可以參考下2024-01-01JPA如何設(shè)置表名和實體名,表字段與實體字段的對應(yīng)
這篇文章主要介紹了JPA如何設(shè)置表名和實體名,表字段與實體字段的對應(yīng),具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-11-11Java實現(xiàn)批量查找與替換Excel文本的思路詳解
在 Java 中,可以通過find和replace的方法來查找和替換單元格的數(shù)據(jù),下面小編將以Excel文件為例為大家介紹如何實現(xiàn)Excel文件內(nèi)容的批量替換,感興趣的朋友跟隨小編一起看看吧2023-10-10