Java面試最容易被刷的重難點(diǎn)之鎖的使用策略
在多線程的學(xué)習(xí)中,很多時(shí)候都要用到鎖,但我們都知道,加鎖這個(gè)操作是一個(gè)計(jì)算機(jī)中開銷比較大的操作,因此,本篇文章我會帶大家學(xué)習(xí)在不同場景中進(jìn)行不同的加鎖處理方式,以讓程序更高效一些有關(guān)鎖策略不僅僅局限于某一種語言,在很多語言中都可能會遇到加鎖操作,而且這部分知識點(diǎn)也是面試中常見的問題,所以本篇文章內(nèi)容基本都是需要大家自己認(rèn)真理解并做到會用自己的語言組織起來的。內(nèi)容均為博主認(rèn)真總結(jié),大家可以收藏起來慢慢學(xué)習(xí),希望可以幫到大家哦!

一. 樂觀鎖和悲觀鎖
1. 字面理解
樂觀鎖認(rèn)為多個(gè)線程訪問同一個(gè)共享數(shù)據(jù)時(shí)產(chǎn)生并發(fā)沖突的概率不大,并不會真的加鎖, 而是直接嘗試訪問數(shù)據(jù), 在訪問的同時(shí)識別當(dāng)前的數(shù)據(jù)是否出現(xiàn)訪問沖突,若沖突,則會返回當(dāng)前的錯誤信息讓用戶去決定如何去處理悲觀鎖會認(rèn)為多個(gè)線程訪問同一個(gè)共享數(shù)據(jù)時(shí)產(chǎn)生并發(fā)沖突的概率很大,因此往往會在取數(shù)據(jù)時(shí)會進(jìn)行加鎖操作,這樣的話其他線程想要拿到這個(gè)數(shù)據(jù)時(shí)就會阻塞等到直到其他線程獲取到鎖
補(bǔ)充:在Java中synchronized這一加鎖操作主要以悲觀鎖為主,初始使用樂觀鎖策略,但當(dāng)發(fā)現(xiàn)鎖競爭比較頻繁的時(shí)候,就會自動切換成悲觀鎖策略
2. 生活實(shí)例
在生活中有很多情況都能涉及到樂觀和悲觀的心態(tài),比如今天是陰天,A認(rèn)為可能會下雨,會提前帶好傘,這就對應(yīng)到了悲觀鎖這一策略;而B比較樂觀,不會認(rèn)為會下雨,因此B不會帶傘,這顯然可以類比為樂觀鎖這一策略。

3. 基于版本號方式實(shí)現(xiàn)樂觀鎖
實(shí)現(xiàn)樂觀鎖策略這一功能的方式有很多,接下來我?guī)Т蠹胰W(xué)習(xí)一種:基于版本號方式。
假設(shè)我們要使用多線程修改用戶的賬戶余額,我們可以引入一個(gè)版本號來實(shí)現(xiàn),具體方法如下:
設(shè)當(dāng)前的余額為100,引入一個(gè)版本號version,將其初始值設(shè)為1,并且我們規(guī)定,提交版本必須大于數(shù)據(jù)庫中記錄的當(dāng)前版本號才能執(zhí)行更新余額的操作,若不滿足此條件,則認(rèn)為修改失敗
圖示
以線程1想把主內(nèi)存中的數(shù)據(jù)減50,線程2把主內(nèi)存中的數(shù)據(jù)減20為例:

線程1此時(shí)準(zhǔn)備將主內(nèi)存中的數(shù)據(jù)讀入自己的工作內(nèi)存中并修改,而線程2也想將主內(nèi)存的數(shù)據(jù)讀入自己的工作內(nèi)存中并修改,此時(shí)線程1和線程2以及主內(nèi)存中的版本號都為1
當(dāng)線程1把主內(nèi)存的數(shù)據(jù)減50后,即修改后,會將自己工作內(nèi)存中的版本號加1,此時(shí)線程1工作內(nèi)存中的版本號大于主內(nèi)存中的版本號(2大于1),因此線程1成功修改了主內(nèi)存中的數(shù)據(jù),并將數(shù)據(jù)50寫入主內(nèi)存中,最后將主內(nèi)存中的版本號加1(即為2)

此時(shí)線程2修改了自己工作內(nèi)存中的數(shù)據(jù),隨后將自己的工作內(nèi)存版本號改為2:

但正當(dāng)線程2準(zhǔn)備將其改好后的數(shù)據(jù)80寫入主內(nèi)存時(shí),發(fā)現(xiàn)自己的版本號和主內(nèi)存的版本號都一樣,并不滿足大于關(guān)系,因此此次修改失敗,有效避免了多線程并發(fā)修改數(shù)據(jù)時(shí)引起的數(shù)據(jù)安全問題。
總結(jié)
基于版本號這樣實(shí)現(xiàn)樂觀鎖的機(jī)制就是一種典型的實(shí)現(xiàn)方式,這個(gè)實(shí)現(xiàn)方式和之前所學(xué)過的單純的互斥的加鎖方式來說更加輕量一些(只修改版本號,只是在計(jì)算機(jī)中用戶態(tài)上進(jìn)行操作,而互斥加鎖方式會涉及到用戶態(tài)和內(nèi)核態(tài)之間的切換,不僅效率不太高,也容易引起線程阻塞)對于這個(gè)機(jī)制來說,如果修改數(shù)據(jù)失敗,就會涉及到重試操作,如果頻繁重試的話那么效率也就不高了,因此,最好在線程并發(fā)沖突率比較低的場景下使用樂觀鎖這一方式比較好
二. 讀寫鎖
1. 理解

我們都知道,當(dāng)我們通過多線程方式嘗試修改同一數(shù)據(jù)時(shí),一般都可能引發(fā)線程安全問題,但當(dāng)我們通過多線程方式嘗試讀取同一數(shù)據(jù)時(shí),一般不會引發(fā)線程安全問題,因此,我們可以根據(jù)讀和寫的不同場景來給讀和寫操作分別加上不同的鎖。
Java當(dāng)中的synchronized不會對讀和寫進(jìn)行區(qū)分,默認(rèn)使用后線程都是互斥的
2. 用法
以Java為例,在標(biāo)準(zhǔn)庫中存在這樣一個(gè)類ReentrantReadWriteLock
源代碼如下
public class ReentrantReadWriteLock
implements ReadWriteLock, java.io.Serializable {
private static final long serialVersionUID = -6992448646407690164L;
/** Inner class providing readlock */
private final ReentrantReadWriteLock.ReadLock readerLock;
/** Inner class providing writelock */
private final ReentrantReadWriteLock.WriteLock writerLock;
/** Performs all synchronization mechanics */
final Sync sync;
/**
* Creates a new {@code ReentrantReadWriteLock} with
* default (nonfair) ordering properties.
*/
public ReentrantReadWriteLock() {
this(false);
}
該類中提供了兩個(gè)方法:
public ReentrantReadWriteLock.WriteLock writeLock() { return writerLock; }
此方法可以創(chuàng)建出一個(gè)讀鎖實(shí)例
public ReentrantReadWriteLock.ReadLock readLock() { return readerLock; }
此方法可以創(chuàng)建出一個(gè)寫鎖實(shí)例
某個(gè)線程被讀鎖修飾后,這兩個(gè)線程之間不會互斥,而是完全同時(shí)并發(fā)執(zhí)行,一般將讀鎖用于線程讀取數(shù)據(jù)比較多的場景;而當(dāng)某個(gè)線程被寫鎖修飾后,這兩個(gè)線程會互斥,一個(gè)線程會執(zhí)行,而另一個(gè)線程會阻塞等待,因此必須是一個(gè)線程執(zhí)行完了,另一個(gè)線程才會執(zhí)行,一般用于修改數(shù)據(jù)比較多的場景
三. 重量級鎖和輕量級鎖
1. 原理
鎖的核心特性 “原子性”,這樣的機(jī)制追根溯源是 CPU 這樣的硬件設(shè)備提供的
1.CPU 提供了 “原子操作指令”。
2. 操作系統(tǒng)基于 CPU 的原子指令,實(shí)現(xiàn)了 mutex 互斥鎖.
3. JVM 基于操作系統(tǒng)提供的互斥鎖。實(shí)現(xiàn)了 synchronized 和 ReentrantLock 等關(guān)鍵字和類。

注意:synchronized 并不僅僅是對 mutex 進(jìn)行封裝, 在 synchronized 內(nèi)部還做了很多其他的工作
2. 理解
1.重量級鎖依賴了OS提供的mutex,的開銷一般很大,往往是通過內(nèi)核來完成的
2.輕量級加鎖一般不使用mutex,開銷一般比較小,一般通過用戶態(tài)就能直接完成
3. 區(qū)分用戶態(tài)和內(nèi)核態(tài)
我們可以類比一個(gè)生活中的例子,當(dāng)去銀行辦理業(yè)務(wù)時(shí),如果是通過用戶在銀行工作人員的指導(dǎo)下自己在窗口外完成,那么效率會比較高,就像計(jì)算機(jī)中的用戶態(tài)一樣。而當(dāng)我們把自己的業(yè)務(wù)交給銀行相關(guān)人員去完成時(shí),由于銀行工作人員的閑忙時(shí)間是不可控的,因此無法保證效率,就好比計(jì)算機(jī)中的內(nèi)核態(tài)。
四. 自旋鎖
1. 理解
當(dāng)兩個(gè)線程為了完成任務(wù)同時(shí)競爭一把鎖時(shí), 拿到鎖的那個(gè)線程會立馬執(zhí)行任務(wù),而沒拿到就會阻塞等待,當(dāng)一個(gè)線程把鎖釋放后,另一個(gè)線程不會被立即喚醒,而是等操作系統(tǒng)將其進(jìn)行一系列的調(diào)度到CPU中的操作才能被喚醒然后執(zhí)行任務(wù),這種鎖叫做掛起等待鎖,線程在搶鎖失敗后進(jìn)入阻塞狀態(tài),放棄 CPU,需要過很久才能再次被調(diào)度。但實(shí)際上,大部分情況下,雖然當(dāng)前搶鎖失敗,但過不了很久,鎖就會被釋放,所以沒必要就放棄 CPU。這個(gè)時(shí)候就可以使用自旋鎖來處理這樣的問題。
2. 實(shí)現(xiàn)方式
自旋鎖的偽代碼為:while (搶鎖(lock) == 失敗) {}
如果獲取鎖失敗,就會立即再嘗試獲取鎖,無限循環(huán),直到獲取到鎖為止。第一次獲取鎖失敗, 第二次的嘗試會在非常短的時(shí)間內(nèi)到來,一旦鎖被其他線程釋放, 就能第一時(shí)間獲取到鎖
3. 優(yōu)缺點(diǎn)
自旋鎖是一種典型的輕量級鎖的實(shí)現(xiàn)方式,它沒有放棄 CPU, 不涉及線程阻塞和調(diào)度,一旦鎖被釋放,就能第一時(shí)間獲取到鎖,這樣會大大提高代碼的執(zhí)行效率,但如果鎖被其他線程持有的時(shí)間比較久, 那么就會持續(xù)地消耗 CPU 資源。(而掛起等待的時(shí)候是不消耗 CPU 的)
因此,我們應(yīng)該注意自旋鎖的適用場合:
- 如果多個(gè)線程執(zhí)行任務(wù)時(shí)鎖的沖突比較低,或者線程持有鎖的時(shí)間比較短,此時(shí)使用自旋鎖比較合適
- 如果某個(gè)線程任務(wù)對CPU比較敏感,且不希望吃太多CPU資源,那么此時(shí)就不太適合使用自旋鎖。
注意:synchronized自身已經(jīng)設(shè)置好了自旋鎖和掛起等待鎖,會根據(jù)不同的情況自動選擇最優(yōu)的使用方案
五. 公平鎖和非公平鎖
1. 理解
若有三個(gè)線程 A,B,C。
A先嘗試獲取鎖,獲取成功了,因?yàn)橹挥幸话焰i,所以B和C線程都會阻塞等待,那么如果A用完了鎖后,B和C線程哪一個(gè)會最先獲取到鎖呢?
- 公平鎖:遵守先來后到原則,因?yàn)锽線程比C線程來的早一點(diǎn),所以B線程先獲取到鎖
- 非公平鎖:沒有先來后到原則,B和C線程獲取到鎖的概率是隨機(jī)的
2. 注意事項(xiàng)
操作系統(tǒng)內(nèi)部的線程調(diào)度就可以視為是隨機(jī)的,如果不做任何額外的限制,鎖就是非公平鎖。如果要想實(shí)現(xiàn)公平鎖,就需要依賴額外的數(shù)據(jù)結(jié)構(gòu)(比如隊(duì)列) 來記錄線程們的先后順序。公平鎖和非公平鎖沒有好壞之分, 關(guān)鍵還是看適用場景(大部分情況下非公平鎖就夠用了,但當(dāng)我們希望線程的調(diào)度時(shí)間成本是可控的,那么此時(shí)就需要用到公平鎖了)
注意:synchronized為非公平鎖
六. 可重入鎖和不可重入鎖
1. 為什么要引入這兩把鎖
(1)實(shí)例一
在介紹可重入鎖和不可重入鎖之前,大家先來思考一個(gè)問題,為什么Java中的main函數(shù)要用static來修飾?
public class Test {
public static void main(String[] args) {
}
}
試想以下,如果main函數(shù)不是static來修飾的話:
public class Test {
public void main(String[] args) {
Test a=new Test();
a.main();
}
}
那么此時(shí)這段代碼能否被執(zhí)行呢?答案是不能,因?yàn)樵趈ava中,沒有static的變量或函數(shù),如果想被調(diào)用的話,是要先新建一個(gè)對象才可以。而main函數(shù)作為程序的入口,需要在其它函數(shù)實(shí)例化之前就啟動,這也就是為什么要加一個(gè)static。main函數(shù)好比一個(gè)門,要探索其它函數(shù)要先從門進(jìn)入程序。static提供了這樣一個(gè)特性,無需建立對象,就可以啟動。也可以利用反證法說明,如果不是static修飾的,若不是靜態(tài)的,main函數(shù)調(diào)用的時(shí)候需要new對象,new完對象才能調(diào)用main函數(shù)。那么你既想進(jìn)入到main函數(shù)new對象,又想new完對象來調(diào)用main函數(shù),那么就不行了,相當(dāng)于自己把自己鎖在里面出不來了
(2)實(shí)例二
另外一個(gè)Java當(dāng)中的例子:
synchronized void func1(){
func2();
}
synchronized void func2(){
}
我們對func1這個(gè)方法進(jìn)行加鎖時(shí),是可以成功的,但當(dāng)我們對func2這個(gè)方法再次加鎖后,就比較危險(xiǎn)了。因?yàn)橐獔?zhí)行完func1這個(gè)方法,就必須執(zhí)行完func2,而此時(shí)鎖已經(jīng)被func1這個(gè)方法占用了,func2獲取不到鎖,所以func2就會一直阻塞等待,去等func1釋放鎖,但func1一直執(zhí)行不完成,所以鎖永遠(yuǎn)不會釋放,func2永遠(yuǎn)也獲取不到鎖,這樣就形成了一個(gè)閉環(huán),相當(dāng)于自己把自己鎖在里面出不來了,此時(shí)這個(gè)線程就會崩潰,是比較危險(xiǎn)的
2. 實(shí)現(xiàn)方案
了解了上面兩個(gè)實(shí)例的嚴(yán)重性后,我們引入了可重入鎖這個(gè)機(jī)制,當(dāng)我們形成死鎖后,如果是可重入鎖的話,它不會讓線程阻塞等待最終死鎖從而奔潰,而是運(yùn)用計(jì)數(shù)器的方法,去記錄當(dāng)前某個(gè)線程針對某把鎖嘗試加了幾次,每加一次鎖計(jì)數(shù)都會加1,每次解鎖計(jì)數(shù)都會減1,這樣當(dāng)計(jì)數(shù)器里面的計(jì)數(shù)完全為0的時(shí)候才會真正釋放鎖,正是因?yàn)橛辛诉@樣的機(jī)制,才有效避免了死鎖問題。而在Java中,synchronized就是一把可重入鎖,它給我們提供了很大的方便,保證在我們即使造成死鎖問題時(shí),程序也不至于崩潰。
七. 面試題
第一題
如何理解樂觀鎖和悲觀鎖,具體實(shí)現(xiàn)方式如何 如何理解?
見樂觀鎖和悲觀鎖字面理解部分(嘗試用自己的語言組織)實(shí)現(xiàn)方式:
(1)樂觀鎖:見基于版本號方式實(shí)現(xiàn)樂觀鎖部分
(2)悲觀鎖:多個(gè)線程訪問同一個(gè)共享數(shù)據(jù)時(shí)產(chǎn)生并發(fā)沖突時(shí),會在取數(shù)據(jù)時(shí)會進(jìn)行加鎖操作,這樣的話其他線程想要拿到這個(gè)數(shù)據(jù)時(shí)就會阻塞等到直到其他線程獲取到鎖
第二題
簡單介紹一下讀寫鎖
讀寫鎖實(shí)際是一種特殊的自旋鎖,它能把同一塊共享數(shù)據(jù)的訪問者分為讀者和寫者,讀寫鎖會把讀操作和寫操作分別進(jìn)行加鎖,且讀鎖和讀鎖之間的線程不會發(fā)生互斥,寫鎖和寫鎖之間以及讀鎖和寫鎖之間的線程會發(fā)生互斥。讀鎖適用于線程讀取數(shù)據(jù)比較多的場景,而寫鎖適用于線程修改數(shù)據(jù)比較多的場景。
第三題
簡單介紹一下自旋鎖
- 理解:當(dāng)兩個(gè)線程為了完成任務(wù)同時(shí)競爭一把鎖時(shí), 拿到鎖的那個(gè)線程會立馬執(zhí)行任務(wù),而沒拿到鎖的線程就會立即再嘗試獲取鎖,無限循環(huán),直到獲取到鎖為止,這樣的鎖就叫自旋鎖
- 優(yōu)點(diǎn)和缺點(diǎn):見自旋鎖優(yōu)缺點(diǎn)部分
第四題
簡單介紹一下Java中synchronized充當(dāng)了哪些鎖
- 主要以悲觀鎖為主,初始使用樂觀鎖策略,但當(dāng)發(fā)現(xiàn)鎖競爭比較頻繁的時(shí)候,就會自動切換成悲觀鎖策略
- 并不區(qū)分讀寫鎖
- synchronized自身已經(jīng)設(shè)置好了自旋鎖和掛起等待鎖,會根據(jù)不同的情況自動選擇最優(yōu)的使用方案
- synchronized是一把非公平鎖
- synchronized就是一把可重入鎖
到此這篇關(guān)于Java面試最容易被刷的重難點(diǎn)之鎖的使用策略的文章就介紹到這了,更多相關(guān)Java 鎖內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Java判斷文件或者文件夾是否存在的功能(不存在創(chuàng)建,存在刪除)
在Java中,要判斷文件或文件夾是否存在,并根據(jù)判斷結(jié)果來執(zhí)行創(chuàng)建或刪除操作,你可以使用java.io.File類或者java.nio.file包中的類,本文介紹Java判斷文件或者文件夾是否存在,不存在創(chuàng)建,存在刪除,感興趣的朋友一起看看吧2025-03-03
Jpa數(shù)據(jù)操作以及@Query和@Modifying注解使用方式
這篇文章主要介紹了Jpa數(shù)據(jù)操作以及@Query和@Modifying注解使用方式,具有很好的參考價(jià)值,希望對大家有所幫助,如有錯誤或未考慮完全的地方,望不吝賜教2024-07-07
IDEA2020導(dǎo)入非maven項(xiàng)目并部署tomcat的方法
這篇文章主要介紹了IDEA 2020 導(dǎo)入非maven項(xiàng)目并部署tomcat的方法,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧2020-07-07
java反射實(shí)現(xiàn)javabean轉(zhuǎn)json實(shí)例代碼
基于java反射機(jī)制實(shí)現(xiàn)javabean轉(zhuǎn)json字符串實(shí)例,大家參考使用吧2013-12-12
利用Spring Session和redis對Session進(jìn)行共享詳解
這篇文章主要給大家介紹了關(guān)于利用Spring、Session和redis對Session進(jìn)行共享的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧。2017-09-09
簡單講解Java的Socket網(wǎng)絡(luò)編程的多播與廣播實(shí)現(xiàn)
這篇文章主要介紹了Java的Socket網(wǎng)絡(luò)編程的多播與廣播實(shí)現(xiàn),包括網(wǎng)絡(luò)編程發(fā)送和接受數(shù)據(jù)的一些基礎(chǔ)知識整理,需要的朋友可以參考下2016-01-01

