Java多線(xiàn)程中常見(jiàn)的鎖策略詳解
1. 悲觀鎖與樂(lè)觀鎖
悲觀鎖:為了保證原子性,因此把數(shù)據(jù)進(jìn)行上鎖,每一個(gè)不同的線(xiàn)程拿數(shù)據(jù)的時(shí)候都會(huì)參與鎖的競(jìng)爭(zhēng),其他線(xiàn)程想必須等待前者拿完數(shù)據(jù)解鎖后才能參與拿數(shù)據(jù)。
舉例,由于維修導(dǎo)致一層樓只剩下一間廁所。因此,線(xiàn)程1進(jìn)入廁所后,其他線(xiàn)程只能阻塞等待。

樂(lè)觀鎖:假設(shè)數(shù)據(jù)一般情況下不會(huì)產(chǎn)生并發(fā)沖突,所以在數(shù)據(jù)進(jìn)行提交更新的時(shí)候,才會(huì)正式對(duì)數(shù)據(jù)是否產(chǎn)生并發(fā)沖突進(jìn)行檢測(cè),如果發(fā)現(xiàn)并發(fā)沖突了,則讓返回用戶(hù)錯(cuò)誤的信息,讓用戶(hù)決定如何去做。
還是上述上廁所例子,線(xiàn)程1 給其他線(xiàn)返回一個(gè)信息,其他線(xiàn)程可根據(jù)信息選擇換樓層上廁所亦或是等待。

以上的上廁所例子,在樓棟廁所不充足情況下。線(xiàn)程還是盲目選擇這棟樓的廁所(使用悲觀鎖)就會(huì)導(dǎo)致阻塞消耗系統(tǒng)資源。在樓棟廁所充足的情況下,線(xiàn)程選擇了這棟樓的廁所(使用樂(lè)觀鎖)這樣就能很好的利用系統(tǒng)資源。
synchronized 初始情況下使用的是樂(lè)觀鎖,當(dāng)發(fā)現(xiàn)鎖競(jìng)爭(zhēng)激烈時(shí)候就會(huì)自動(dòng)轉(zhuǎn)換為悲觀鎖。就好比一個(gè)線(xiàn)程去某一棟樓上廁所,并不知道該樓棟是否廁所充足。充足就不阻塞等待,不充足就阻塞等待。
2. 讀寫(xiě)鎖與互斥鎖
多線(xiàn)程中,線(xiàn)程作為讀取方不會(huì)產(chǎn)生線(xiàn)程安全問(wèn)題,當(dāng)線(xiàn)程作為為寫(xiě)入方和線(xiàn)程作為寫(xiě)入方之間進(jìn)行交互和,線(xiàn)程作為寫(xiě)入方和線(xiàn)程作為讀取方之間進(jìn)行交互,就會(huì)造成互斥。
線(xiàn)程對(duì)數(shù)據(jù)的訪(fǎng)問(wèn),主要存在三種情況:
- 線(xiàn)程只是對(duì)數(shù)據(jù)進(jìn)行讀操作,此時(shí)自然不會(huì)出現(xiàn)線(xiàn)程不安全問(wèn)題。
- 多個(gè)線(xiàn)程對(duì)數(shù)據(jù)進(jìn)行寫(xiě)操作,就會(huì)出現(xiàn)線(xiàn)程不安全問(wèn)題。
- 一個(gè)線(xiàn)程對(duì)數(shù)據(jù)進(jìn)行讀操作,另個(gè)線(xiàn)程對(duì)數(shù)據(jù)進(jìn)行寫(xiě)操作,也會(huì)出現(xiàn)線(xiàn)程不安全問(wèn)題。
簡(jiǎn)單的來(lái)說(shuō),線(xiàn)程的讀操作,就是線(xiàn)程對(duì)數(shù)據(jù)進(jìn)行訪(fǎng)問(wèn)。線(xiàn)程的寫(xiě)操作,就是線(xiàn)程對(duì)數(shù)據(jù)進(jìn)行修改。讀一下問(wèn)題不大,但寫(xiě)一下就難免會(huì)造成意外。因此,我們有了 讀寫(xiě)鎖 這個(gè)概念。
讀寫(xiě)鎖,就是把讀和寫(xiě)這兩個(gè)操作分開(kāi)來(lái)加鎖這樣就能避免互斥。Java 標(biāo)準(zhǔn)庫(kù)提供了一個(gè) ReentrantReadWriteLock 類(lèi),實(shí)現(xiàn)了讀寫(xiě)鎖。
ReentrantReadWriteLock.ReadLock 類(lèi)表示一個(gè)讀鎖.。這個(gè)對(duì)象提供了 lock / unlock 方法進(jìn)行加鎖解鎖。
ReentrantReadWriteLock.WriteLock 類(lèi)表示一個(gè)寫(xiě)鎖.。這個(gè)對(duì)象也提供了 lock / unlock 方法進(jìn)行加鎖解鎖。
- 讀加鎖與讀加鎖之間,不互斥
- 寫(xiě)加鎖與寫(xiě)加鎖之間,互斥
- 讀加鎖與寫(xiě)加鎖之間,互斥
互斥,就會(huì)操作線(xiàn)程的掛起等待,一旦線(xiàn)程掛起等待了,就不知道什么時(shí)候能夠被喚醒了。因此,我們?cè)诰帉?xiě)代碼的時(shí)候盡可能減少互斥。
讀寫(xiě)鎖特別適用于“頻繁讀,不頻繁寫(xiě)”的場(chǎng)景中。比如,學(xué)校的教務(wù)系統(tǒng):
假設(shè)計(jì)算機(jī)軟件專(zhuān)業(yè)的學(xué)生有 300 個(gè)同學(xué),這300個(gè)同學(xué)幾乎每天都要課程表為了防止課表更改,這樣的一個(gè)操作就是頻繁讀(訪(fǎng)問(wèn))。
有特殊情況,老師生病了或是怎樣,偶爾會(huì)調(diào)課到其他時(shí)間點(diǎn)。這樣的操作,就是不頻繁寫(xiě)(修改)。
注意,synchronized 不是讀寫(xiě)鎖。
3. 重量級(jí)鎖與輕量級(jí)鎖
在并發(fā)編程中,輕量級(jí)鎖和重量級(jí)鎖是兩種鎖的實(shí)現(xiàn)方法,主要用于解決多個(gè)線(xiàn)程同時(shí)訪(fǎng)問(wèn)共享資源時(shí)的同步問(wèn)題。
輕量級(jí)鎖通常用于鎖競(jìng)爭(zhēng)不激烈的情況下,通過(guò)在線(xiàn)程內(nèi)部使用CAS操作來(lái)進(jìn)行加鎖和解鎖,這種方式不需要進(jìn)行線(xiàn)程的上下文切換,因此性能比重量級(jí)鎖更高。但是,如果鎖競(jìng)爭(zhēng)激烈的話(huà),輕量級(jí)鎖的性能優(yōu)勢(shì)就不明顯了。
重量級(jí)鎖通常用于鎖競(jìng)爭(zhēng)激烈的情況下,通過(guò)將競(jìng)爭(zhēng)鎖的線(xiàn)程掛起并切換到內(nèi)核態(tài)來(lái)進(jìn)行加鎖和解鎖。由于需要進(jìn)行線(xiàn)程的上下文切換,因此性能比輕量級(jí)鎖更低。但是,在鎖競(jìng)爭(zhēng)激烈的情況下,重量級(jí)鎖的效果要比輕量級(jí)鎖好得多,因?yàn)樗梢杂行У乇苊怄i爭(zhēng)用問(wèn)題,減少了線(xiàn)程的搶占和切換,從而提高了系統(tǒng)的效率和響應(yīng)速度。
synchronized 的輕量級(jí)鎖策略大概都是通過(guò)自旋鎖的方式實(shí)現(xiàn)的,重量級(jí)鎖則是掛起等待鎖。
4. 自旋鎖與掛起等待鎖
自旋鎖 VS 掛起等待鎖:
自旋鎖,當(dāng)線(xiàn)程之間進(jìn)行搶占鎖內(nèi)資源時(shí)候,線(xiàn)程1 已經(jīng)搶占到鎖,線(xiàn)程2 則會(huì)持續(xù)等待 線(xiàn)程1 鎖內(nèi)任務(wù)結(jié)束后再進(jìn)行搶占鎖資源,在這期間 線(xiàn)程2 持續(xù)處于阻塞等待狀態(tài)。
掛起等待鎖,當(dāng)掛起等待鎖遇到這種情況時(shí),發(fā)現(xiàn)有線(xiàn)程已經(jīng)搶占到鎖了,則會(huì)放棄阻塞等待。直到鎖開(kāi)放了,則再參與搶占鎖。
因此,自旋鎖有以下優(yōu)缺點(diǎn):
- 優(yōu)點(diǎn):時(shí)刻占用系統(tǒng)資源,不涉及線(xiàn)程阻塞和調(diào)度,一旦鎖被釋放了,參與鎖的競(jìng)爭(zhēng)。
- 缺點(diǎn):當(dāng)鎖內(nèi)任務(wù)比較復(fù)雜時(shí),鎖被其他線(xiàn)程占有時(shí)間過(guò)長(zhǎng),那么就會(huì)持續(xù)消耗系統(tǒng)資源。
- 掛起等待鎖則相反
4.1 自旋鎖
自旋鎖,按照正常的邏輯,當(dāng)線(xiàn)程搶占鎖時(shí)進(jìn)入阻塞狀態(tài),過(guò)不了多久鎖就被釋放了。因此,自旋鎖就沒(méi)必要放棄 CPU 了,一直占用著 CPU 的內(nèi)存空間。
自旋鎖偽代碼:
while(槍鎖lock == 失敗) {}如果獲取鎖失敗,立即再?lài)L試獲取鎖,無(wú)限循環(huán)下去直到獲取到鎖為止。第一次獲取鎖失敗,往后的獲取鎖操作會(huì)在極短的時(shí)間內(nèi)到來(lái),一旦鎖被其他線(xiàn)程釋放,就能第一時(shí)間獲取到鎖。這就是輕量級(jí)鎖的體現(xiàn)(鎖的競(jìng)爭(zhēng)還不太激烈,嘗試使用自旋方式加鎖)。
4.2 掛起等待鎖
當(dāng)線(xiàn)程獲取鎖失敗后,并不會(huì)進(jìn)行阻塞等待。而隨著系統(tǒng)的調(diào)度,不占用 CPU 。直到鎖開(kāi)發(fā)后,再?lài)L試參與鎖競(jìng)爭(zhēng)。這種情況就是掛起等待鎖,也是重量級(jí)鎖的體現(xiàn)(鎖的競(jìng)爭(zhēng)太激烈了,線(xiàn)程跟隨系統(tǒng)的調(diào)度)。
舉例:
自旋鎖與掛起等待鎖的現(xiàn)實(shí)生活體現(xiàn):張三是一個(gè)普通的男生,如花是一個(gè)漂亮的女孩,在此張三作為線(xiàn)程,如花作為鎖。
張三開(kāi)始追求如花,但是如花已經(jīng)有男朋友了。張三又是個(gè)死皮賴(lài)臉的人。每天堅(jiān)持給女孩發(fā)信息,期待著某一天如花分手,能得到如花。此時(shí)張三就處于自旋鎖的狀態(tài)。
隨著競(jìng)爭(zhēng)的激烈,又有許多人想要追求如花。張三開(kāi)始動(dòng)搖了,開(kāi)始努力敲代碼、認(rèn)真學(xué)習(xí)不參與追如花的競(jìng)爭(zhēng)了(隨著系統(tǒng)的調(diào)度做其他事去了)。如果某一天如花變?yōu)閱紊砹?,系統(tǒng)會(huì)通知張三如花單身了(鎖空閑了),張三就又開(kāi)始參與競(jìng)爭(zhēng)鎖。此時(shí)張三的狀態(tài)就是掛起等待鎖狀態(tài)。
5. 公平鎖與非公平鎖
公平鎖與非公平鎖講究四個(gè)字“公平競(jìng)爭(zhēng)”,假設(shè)有三個(gè)線(xiàn)程搶占鎖資源,當(dāng)鎖被釋放后就會(huì)出現(xiàn)兩種情況:公平競(jìng)爭(zhēng)鎖、非公平競(jìng)爭(zhēng)鎖。
公平鎖:遵循先來(lái)后到的原則,線(xiàn)程1 進(jìn)入鎖,鎖釋放后。線(xiàn)程2 進(jìn)入鎖,鎖再釋放后。線(xiàn)程3 進(jìn)入鎖。整個(gè)過(guò)程是按照順序執(zhí)行的。
非公平鎖:由于線(xiàn)程之間搶占資源,導(dǎo)致鎖被無(wú)序的搶占。這樣 3 個(gè)線(xiàn)程都有機(jī)會(huì)優(yōu)先進(jìn)入鎖。整個(gè)過(guò)程會(huì)造成無(wú)序執(zhí)行。

通過(guò)上圖我們就能很好的理解,公平鎖與非公平鎖之間的差異。當(dāng)然,線(xiàn)程的調(diào)度是隨機(jī)的因此多個(gè)線(xiàn)程競(jìng)爭(zhēng)鎖時(shí)可以隨意進(jìn)行搶占“手快有,手慢無(wú)”(非公平鎖)。要想實(shí)現(xiàn)公平鎖,我們可以使用一些特定的數(shù)據(jù)結(jié)構(gòu)來(lái)達(dá)到按順序使用鎖。
在實(shí)際開(kāi)發(fā)中,公平鎖與非公平鎖沒(méi)有好壞之分,我們按照需求來(lái)進(jìn)行設(shè)置。注意,synchronized 屬于非公平鎖。
6. 可重入鎖與不可重入鎖
可重入鎖即允許一個(gè)線(xiàn)程多次獲取同一把鎖。
不可重入鎖是指一旦線(xiàn)程獲得了該鎖,此時(shí)再次請(qǐng)求獲取該鎖時(shí),系統(tǒng)會(huì)將該線(xiàn)程掛起,直到該鎖被釋放為止。因此,不可重入鎖不能再同一線(xiàn)程中重復(fù)獲取。
可重入鎖是指當(dāng)一個(gè)線(xiàn)程獲得了該鎖之后,在該鎖還未釋放之前,可以再次獲取該鎖。這種鎖可以防止死鎖的發(fā)生,因?yàn)樵讷@取之后可以在方法中重新獲取該鎖,從而避免死鎖的發(fā)生。
Java 中的 synchronized 關(guān)鍵字是一種可重入鎖,而 ReentrantLock 是 Java 中常用的可重入鎖類(lèi),synchronized 不需要手動(dòng)解鎖,而 ReentrantLock 需要手動(dòng)解鎖。
需要注意的是,可重入鎖雖然提高了代碼的靈活性和可維護(hù)性,但同時(shí)也可能會(huì)帶來(lái)出現(xiàn)深度嵌套鎖的風(fēng)險(xiǎn),引發(fā)死鎖或性能下降等問(wèn)題。因此,在使用可重入鎖時(shí)需要仔細(xì)設(shè)計(jì)和管理。
談?wù)勀銓?duì)synchronized的演變過(guò)程的理解?
synchronized 既是悲觀鎖也是樂(lè)觀鎖,synchronized 即是輕量級(jí)鎖也是重量級(jí)鎖,synchronized 即是自旋鎖也是掛起等待鎖,synchronized 不是讀寫(xiě)鎖,synchronized 是非公平鎖,synchronized是可重入鎖。
synchronized 的初始化的時(shí)候是一個(gè)樂(lè)觀鎖/輕量級(jí)鎖/自旋鎖,隨著synchronized的競(jìng)爭(zhēng)激烈會(huì)升級(jí)為悲觀鎖/重量級(jí)鎖/掛起等待鎖,另外輕量級(jí)鎖是部分基于自旋鎖、重量級(jí)鎖是部分基于掛起等待鎖。
在鎖的策略中還會(huì)引申到“死鎖”的概念,在下篇博文中,我會(huì)介紹。大家也可以通過(guò)下方專(zhuān)欄中搜索多線(xiàn)程相關(guān)內(nèi)容。
到此這篇關(guān)于Java多線(xiàn)程中常見(jiàn)的鎖策略詳解的文章就介紹到這了,更多相關(guān)Java常見(jiàn)的鎖策略?xún)?nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
java.lang.IllegalStateException異常原因和解決辦法
這篇文章主要給大家介紹了關(guān)于java.lang.IllegalStateException異常原因和解決辦法,IllegalStateException是Java標(biāo)準(zhǔn)庫(kù)中的一個(gè)異常類(lèi),通常表示在不合適或無(wú)效的情況下執(zhí)行了某個(gè)方法或操作,需要的朋友可以參考下2023-07-07
Spring自動(dòng)配置之condition條件判斷下篇
這篇文章主要為大家介紹了SpringBoot?condition條件判斷功能的使用,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2022-08-08
Kafka是什么及如何使用SpringBoot對(duì)接Kafka(最新推薦)
這篇文章主要介紹了Kafka是什么,以及如何使用SpringBoot對(duì)接Kafka,今天我們通過(guò)一個(gè)Demo講解了在SpringBoot中如何對(duì)接Kafka,也介紹了下關(guān)鍵類(lèi)?KafkaTemplate,需要的朋友可以參考下2023-11-11
SpringBoot動(dòng)態(tài)生成接口實(shí)現(xiàn)流程示例講解
最近遇到一個(gè)需求,需要在程序運(yùn)行過(guò)程中,可以動(dòng)態(tài)新增接口,自定義接口參數(shù)名稱(chēng),基本類(lèi)型,以及請(qǐng)求方法,請(qǐng)求頭等等。通過(guò)幾天的研究,找到了我需要的解決方案2023-01-01
Java通過(guò)apache poi生成excel實(shí)例代碼
本篇文章主要介紹了Java通過(guò)apache poi生成excel實(shí)例代碼,小編覺(jué)得挺不錯(cuò)的,現(xiàn)在分享給大家,也給大家做個(gè)參考。一起跟隨小編過(guò)來(lái)看看吧2017-06-06
SpringBoot中使用AOP切面編程實(shí)現(xiàn)登錄攔截功能
本文介紹了如何使用AOP切面編程實(shí)現(xiàn)Spring Boot中的登錄攔截,通過(guò)實(shí)例代碼給大家介紹的非常詳細(xì),感興趣的朋友一起看看吧2024-12-12

