Java面試題沖刺第二十四天--并發(fā)編程
面試題1:說(shuō)一下你對(duì)ReentrantLock的理解?
ReentrantLock是JDK1.5引入的,它擁有與synchronized相同的并發(fā)性和內(nèi)存語(yǔ)義,并提供了超出synchonized的其他高級(jí)功能(例如,中斷鎖等候、條件變量等),并且使用ReentrantLock比synchronized能獲得更好的可伸縮性。
ReentrantLock主要利用: CAS(compare-and-swap) + AQS(AbstractQueuedSynchronizer)隊(duì)列來(lái)實(shí)現(xiàn)。它支持公平鎖和非公平鎖,兩者的實(shí)現(xiàn)類似。
CAS:
CAS(compare-and-swap),見(jiàn)名知意,比較并交換。CAS 加 volatile 關(guān)鍵字是實(shí)現(xiàn)并發(fā)包的基石。沒(méi)有CAS就不會(huì)有并發(fā)包,synchronized是一種獨(dú)占鎖、悲觀鎖,java.util.concurrent中借助了CAS指令實(shí)現(xiàn)了一種區(qū)別于synchronized的一種樂(lè)觀鎖。
CAS引用了樂(lè)觀鎖思想,每次拿數(shù)據(jù)的時(shí)候都認(rèn)為別的線程不會(huì)修改這個(gè)數(shù)據(jù),所以不會(huì)上鎖,但是在更新的時(shí)候會(huì)通過(guò)標(biāo)記參數(shù)判斷一下在此期間(更新期間)別的線程有沒(méi)有已經(jīng)修改過(guò)該標(biāo)記數(shù)據(jù),如果發(fā)現(xiàn)有其他線程在修改且未修改完成,并不會(huì)像悲觀鎖那樣阻塞線程,而是直接返回,可以去選擇再次重試獲得鎖,也可以直接退出。
舉個(gè)流程示例
如CAS操作包括三個(gè)操作數(shù):需要讀寫(xiě)的內(nèi)存位置(V)、預(yù)期原值(A)、新值(B)。如果內(nèi)存位置與預(yù)期原值的A相匹配,說(shuō)明在此期間(更新期間)別的線程未修改過(guò)該標(biāo)記數(shù)據(jù),那么將內(nèi)存位置的值更新為新值B。如果內(nèi)存位置與預(yù)期原值的值不匹配,那么處理器不會(huì)做任何操作。
無(wú)論哪種情況,它都會(huì)在 CAS 指令之前返回該位置的值。(在 CAS 的一些特殊情況下將僅返回 CAS 是否成功,而不提取當(dāng)前值。)
AQS:
AQS主要利用硬件原語(yǔ)指令CAS,來(lái)實(shí)現(xiàn)輕量級(jí)多線程同步機(jī)制,并且不會(huì)引起CPU上文切換和調(diào)度,同時(shí)提供內(nèi)存可見(jiàn)性和原子化更新保證(線程安全的三要素:原子性、可見(jiàn)性、順序性)。
AQS的本質(zhì)上是一個(gè)同步器/阻塞鎖的基礎(chǔ)框架,其作用主要是提供加鎖、釋放鎖,并在內(nèi)部維護(hù)一個(gè)FIFO等待隊(duì)列,用于存儲(chǔ)由于鎖競(jìng)爭(zhēng)而阻塞的線程。
追問(wèn)1:你認(rèn)為 ReentrantLock 相比 synchronized 都有哪些區(qū)別?
優(yōu)秀問(wèn)答摘自:https://ask.csdn.net/questions/1101634
兩者的共同點(diǎn):
- 都是用來(lái)協(xié)調(diào)多線程對(duì)共享對(duì)象、變量的訪問(wèn)
- 都是可重入鎖,同一線程可以多次獲得同一個(gè)鎖
- 都保證了可見(jiàn)性和互斥性
兩者的不同點(diǎn):
- ReentrantLock 顯示的獲得、釋放鎖,synchronized 隱式獲得釋放鎖;
- ReentrantLock 可響應(yīng)中斷、可輪回,synchronized 是不可以響應(yīng)中斷的,為處理鎖的不可用性提供了更高的靈活性;
- ReentrantLock 是API 級(jí)別的,synchronized 是 JVM 級(jí)別的;
- ReentrantLock 可以實(shí)現(xiàn)公平鎖;
- ReentrantLock 通過(guò) Condition 可以綁定多個(gè)條件;
- 底層實(shí)現(xiàn)不一樣, synchronized 是同步阻塞,使用的是悲觀并發(fā)策略,ReentrantLock 是同步非阻塞,采用的是樂(lè)觀并發(fā)策略;
- Lock 是一個(gè)接口,而 synchronized 是 Java 中的關(guān)鍵字,synchronized 是內(nèi)置的語(yǔ)言實(shí)現(xiàn);
- synchronized 在發(fā)生異常時(shí),會(huì)自動(dòng)釋放線程占有的鎖,因此不會(huì)導(dǎo)致死鎖現(xiàn)象發(fā)生;
- 而 Lock 在發(fā)生異常時(shí),如果沒(méi)有主動(dòng)通過(guò) unLock()去釋放鎖,則很可能造成死鎖現(xiàn)象,因此使用 Lock 時(shí)需要在 finally 塊中釋放鎖。
Lock lock = new ReentrantLock(); Condition condition = lock.newCondition(); lock.lock(); try { while(條件判斷表達(dá)式) { condition.wait(); } // 處理邏輯 } finally { lock.unlock(); }
- Lock 可以讓等待鎖的線程響應(yīng)中斷,而 synchronized 卻不行,使用 synchronized 時(shí),等待的線程會(huì)一直等待下去,不能夠響應(yīng)中斷。
- 通過(guò) Lock 可以知道有沒(méi)有成功獲取鎖,而 synchronized 卻無(wú)法辦到。
- Lock 可以提高多個(gè)線程進(jìn)行讀操作的效率,就是實(shí)現(xiàn)讀寫(xiě)鎖等。
面試題2:解釋一下公平鎖和非公平鎖?
ReenTrantLock可以指定是公平鎖還是非公平鎖,而synchronized只能是非公平鎖。所謂的公平鎖就是先等待的線程先獲鎖。
我們剛才提及到AQS中,如果線程A正處于lock狀態(tài),線程B進(jìn)來(lái)時(shí)發(fā)現(xiàn)線程A處于lock狀態(tài),會(huì)自動(dòng)進(jìn)入阻塞隊(duì)列,等待取鎖;這時(shí)候當(dāng)線程C也進(jìn)來(lái)了也發(fā)現(xiàn)線程A處于lock狀態(tài),也會(huì)自動(dòng)進(jìn)入阻塞隊(duì)列。那么等A釋放鎖后,下次加鎖到底是線程B先拿到還是線程C先拿到呢?
ReentrantLock有個(gè)構(gòu)造方法用于設(shè)置鎖的公平性,如果我們僅僅是new了一個(gè)ReentrantLock的話,那么就是非公平鎖(默認(rèn)),就是靠自己去爭(zhēng)取,完全的隨機(jī)性。如果我們?cè)趎ew ReentrantLock(true) 加入 true參數(shù)時(shí),公平鎖,就會(huì)遵循先入先出的原則,保證了鎖的公平性。
面試題3:能詳細(xì)說(shuō)一下CAS具體實(shí)現(xiàn)原理么?
首先,CAS的英文單詞是Compare and Swap,即是比較并替換。CAS機(jī)制中使用了3個(gè)基本操作數(shù):內(nèi)存地址V,舊的預(yù)期值A(chǔ),待替換的新值B。
CAS規(guī)則是:當(dāng)需要更新一個(gè)變量的值的時(shí)候,只有當(dāng)變量的預(yù)期值A(chǔ)和內(nèi)存地址V中的實(shí)際值相同的時(shí)候,才會(huì)把內(nèi)存地址V對(duì)應(yīng)的值替換成B。
下面我們通過(guò)一個(gè)例子來(lái)講解:
1.在內(nèi)存地址V中存儲(chǔ)的值是陳哈哈
2.線程01想要把變量的值改成僑總,對(duì)于線程01而言,內(nèi)存地址 V=‘陳哈哈’,舊的預(yù)期值 A=‘陳哈哈’,需要替換的新值 B=‘僑總’。
3.在線程01要提交更新之前,另外一個(gè)線程02搶先一步,將內(nèi)存地址V中的值更新成了V='比特幣'。
4.線程01開(kāi)始提交更新的時(shí)候,按照CAS機(jī)制,首先進(jìn)行A的值與內(nèi)存地址V中的值進(jìn)行比較( A=‘陳哈哈’ V=‘比特幣’),發(fā)現(xiàn) A != V 中的實(shí)際值,提交失敗。
5.線程01未獲取鎖后進(jìn)行重試,重新獲取內(nèi)存地址V的當(dāng)前值(V=‘比特幣’),并重新賦值想要修改的值(B=‘僑總’)。截至目前,線程01舊的預(yù)期值為A='比特幣',B='僑總',這個(gè)重新嘗試的過(guò)程被稱為自旋。
6.這一次就比較順利了,沒(méi)有其他線程改變?cè)撟兞康闹担跃€程01通過(guò)CAS機(jī)制,比較舊的預(yù)期值A(chǔ)與內(nèi)存地址V的值,相同(V == A),可以替換。
7.線程01進(jìn)行替換,把地址V(V=‘比特幣’)的值替換成B(B=‘僑總’)。
以上就是一個(gè)比較完整的CAS鎖沖突的處理方式。
從思想上來(lái)看,synchronized屬于悲觀鎖,悲觀的認(rèn)為程序中的并發(fā)問(wèn)題十分嚴(yán)重,所以嚴(yán)防死守,只讓一個(gè)線程操作該代碼塊。而CAS屬于樂(lè)觀鎖,樂(lè)觀地認(rèn)為程序中的并發(fā)問(wèn)題并不那么嚴(yán)重,所以讓線程不斷的去嘗試更新,在并發(fā)問(wèn)題不嚴(yán)重的時(shí)候性能要比synchronized快。
追問(wèn)1:那CAS的缺陷有哪些呢?
當(dāng)然,CAS也有缺點(diǎn),如ABA問(wèn)題,自旋鎖消耗問(wèn)題、多變量共享一致性問(wèn)題等。
1.ABA:
問(wèn)題描述:
線程t1將它的值從A變?yōu)锽,再?gòu)腂變?yōu)锳。同時(shí)有線程t2要將值從A變?yōu)镃。但CAS檢查的時(shí)候會(huì)發(fā)現(xiàn)沒(méi)有改變,但是實(shí)質(zhì)上它已經(jīng)發(fā)生了改變 ??赡軙?huì)造成數(shù)據(jù)的缺失。
解決方法:
CAS還是類似于樂(lè)觀鎖,同數(shù)據(jù)樂(lè)觀鎖的方式給它加一個(gè)版本號(hào)或者時(shí)間戳,如AtomicStampedReference
2.自旋消耗資源:
問(wèn)題描述:
多個(gè)線程爭(zhēng)奪同一個(gè)資源時(shí),如果自旋一直不成功,將會(huì)一直占用CPU。
解決方法:
破壞掉for死循環(huán),當(dāng)超過(guò)一定時(shí)間或者一定次數(shù)時(shí),return退出。JDK8新增的LongAddr,和ConcurrentHashMap類似的方法。當(dāng)多個(gè)線程競(jìng)爭(zhēng)時(shí),將粒度變小,將一個(gè)變量拆分為多個(gè)變量,達(dá)到多個(gè)線程訪問(wèn)多個(gè)資源的效果,最后再調(diào)用sum把它合起來(lái)。
雖然base和cells都是volatile修飾的,但感覺(jué)這個(gè)sum操作沒(méi)有加鎖,可能sum的結(jié)果不是那么精確。
3.多變量共享一致性問(wèn)題:
解決方法:
CAS操作是針對(duì)一個(gè)變量的,如果對(duì)多個(gè)變量操作,
- 可以加鎖來(lái)解決。
- 封裝成對(duì)象類解決。
追問(wèn)2:講一下什么是ABA問(wèn)題?怎么解決?
ABA:如果另一個(gè)線程修改V值假設(shè)原來(lái)是A,先修改成B,再修改回成A。當(dāng)前線程的CAS操作無(wú)法分辨當(dāng)前V值是否發(fā)生過(guò)變化。
舉個(gè)例子1:
例一:你和女神一起喝茶,女神喝了一半去廁所了,你猥瑣的喝了她剩下的半杯,然后又從你杯子里倒了半杯給她,女神回來(lái)后也不知道是否被人喝過(guò)。
如果覺(jué)得例子1太猥瑣的話,請(qǐng)看例子2:
例二:
線程1首先執(zhí)行成功,把余額10000更新為5000(取錢線程)。同時(shí)線程2由于某種原因陷入了阻塞狀態(tài)(取錢線程)。這時(shí)候,(線程3)僑總匯款給了我5000元,執(zhí)行成功,我的賬戶5000更新為10000元。過(guò)一會(huì)兒,線程2恢復(fù)運(yùn)行,由于之前阻塞的時(shí)候獲得了當(dāng)前值為:10000,并且經(jīng)過(guò)compare檢測(cè),此時(shí)存款也的確是10000元,所以又成功把變量值從10000更新成了5000。僑總:哈哥,5000給你打過(guò)去了!我:查到賬戶只有5000,你他娘的糊弄老子呢???jī)S總:???我好幾十個(gè)比特幣的人,還在乎你這5000塊錢了。。。
今天上午10:30:00:我銀行卡有一萬(wàn)塊錢,今天我來(lái)ATM機(jī)取5000塊出來(lái)買比特幣,但由于ATM機(jī)硬件問(wèn)題,導(dǎo)致取款操作同時(shí)提交了兩遍,后臺(tái)開(kāi)啟了兩個(gè)線程(線程1、線程2),兩個(gè)線程都是獲取當(dāng)前值10000元,要更新成5000元;理想情況下,應(yīng)該一個(gè)線程更新成功,一個(gè)線程更新失敗,我的存款只扣除一次,也就是余額應(yīng)為5000元 。
好巧不巧,也是今天上午10:30:00:僑總上次買幣欠我5000塊,經(jīng)過(guò)我再三催債,表示再不還錢就把你買幣的事兒告訴你媳婦!也正巧這個(gè)點(diǎn)兒,僑總給我轉(zhuǎn)了5000元到卡里(線程3)。沒(méi)想到這再正常不過(guò)的事兒,缺被ABA問(wèn)題坑了!我可忍不了!
事情是這樣的:
這就是經(jīng)典的 A → B → A 問(wèn)題,通過(guò)上面的例子,相信同學(xué)們也基本了解它的原理了。其實(shí)解決方式也很簡(jiǎn)單,比如例一,我們將每一次倒水假設(shè)有一個(gè)自動(dòng)記錄儀記錄下,這樣主人回來(lái)就可以分辨在她離開(kāi)后是否發(fā)生過(guò)重新倒?jié)M的情況。這也是解決ABA問(wèn)題目前采用的策略。
使用版本號(hào),通過(guò)比較值 + 版本號(hào)才判斷是否可以替換。這么看來(lái)如果要解決ABA問(wèn)題,就需要在CAS基礎(chǔ)上在增加一個(gè)版本號(hào)的校驗(yàn),當(dāng)值 + 版本號(hào)都相等時(shí),才進(jìn)行替換,其他部分均不變。
而在Java中,AtomicStampedReference類就實(shí)現(xiàn)了用版本號(hào)做比較的CAS機(jī)制。狗子們,你心里有數(shù)了么?
總結(jié)
本篇文章就到這里了,希望能給你帶來(lái)幫助,也希望您關(guān)注腳本之家的更多內(nèi)容!
- Java并發(fā)編程之代碼實(shí)現(xiàn)兩玩家交換裝備
- Java并發(fā)編程之阻塞隊(duì)列(BlockingQueue)詳解
- java實(shí)戰(zhàn)案例之用戶注冊(cè)并發(fā)送郵件激活/發(fā)送郵件驗(yàn)證碼
- JAVA并發(fā)圖解
- java并發(fā)編程JUC CountDownLatch線程同步
- Java并發(fā)之Condition案例詳解
- java并發(fā)編程之ThreadLocal詳解
- Java 處理高并發(fā)負(fù)載類優(yōu)化方法案例詳解
- 淺談Java高并發(fā)解決方案以及高負(fù)載優(yōu)化方法
- Java httpClient連接池支持多線程高并發(fā)的實(shí)現(xiàn)
- Java中常見(jiàn)的并發(fā)控制手段淺析
- Java之Rsync并發(fā)遷移數(shù)據(jù)并校驗(yàn)詳解
- Java 模擬真正的并發(fā)請(qǐng)求詳情
相關(guān)文章
Canal搭建?idea設(shè)置及采集數(shù)據(jù)到kafka的操作方法
這篇文章主要介紹了Canal搭建idea設(shè)置及采集數(shù)據(jù)到kafka的相關(guān)知識(shí),本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借鑒價(jià)值,需要的朋友可以參考下2023-05-05java中實(shí)體類實(shí)現(xiàn)時(shí)間日期自動(dòng)轉(zhuǎn)換方式
這篇文章主要介紹了java中實(shí)體類實(shí)現(xiàn)時(shí)間日期自動(dòng)轉(zhuǎn)換方式,具有很好的參考價(jià)值,希望對(duì)大家有所幫助,如有錯(cuò)誤或未考慮完全的地方,望不吝賜教2024-06-06springboot 配置DRUID數(shù)據(jù)源的方法實(shí)例分析
這篇文章主要介紹了springboot 配置DRUID數(shù)據(jù)源的方法,結(jié)合實(shí)例形式分析了springboot 配置阿里DRUID數(shù)據(jù)源的具體步驟與相關(guān)操作技巧,需要的朋友可以參考下2019-12-12