三道java新手入門面試題,通往自由的道路--鎖+Volatile
我們先看一組代碼:
public class VolatileVisibleDemo { public static boolean initFlag = false; public static void main(String[] args) {new Thread(new Runnable() { @Override public void run() {System.out.println('等待initFlag改變?。?!');// 如果initFlag發(fā)生改變了,這是為true的話,才會結(jié)束循環(huán)while(!initFlag) {}System.out.println('今天的世界打烊了,晚安!'); }}).start();// 這里是為了能保證運(yùn)行完上面的代碼try { Thread.sleep(1000);} catch (InterruptedException e) { e.printStackTrace();}// 這里是Lambda表達(dá)式,就是上面的縮寫new Thread(() -> { System.out.println('準(zhǔn)備填充數(shù)據(jù),修改initFlag的值'); initFlag = true; System.out.println('準(zhǔn)備數(shù)據(jù)完了!');}).start(); }}
運(yùn)行得到的結(jié)果:
我們可以發(fā)現(xiàn),其實(shí)在準(zhǔn)備數(shù)據(jù)完后,我們的initFlag的變量其實(shí)已經(jīng)改變,但是為什么還是沒有結(jié)束循環(huán)輸出**今天的世界打烊了,晚安!**這一句呢?
從之間的JMM模型,我們可以知道,不同線程之間是不能直接訪問對方工作內(nèi)存中的變量,線程間變量的值的傳遞需要通過主內(nèi)存中轉(zhuǎn)來完成,并且線程在修改完數(shù)值后,也不是馬上同步到主內(nèi)存中,并且另一個線程也是無法感知到數(shù)據(jù)發(fā)生改變的,所以就會有可見性問題。
那我們可以加個volatile關(guān)鍵字修飾變量試下?
public static volatile boolean initFlag = false;
我們可以發(fā)現(xiàn):
在我們的變量修飾了volatile關(guān)鍵字后,就能輸出**今天的世界打烊了,晚安!**這一句了。
我們來看看圖解吧:
先解釋下這其中連接的幾個單詞:
read(讀取):從主內(nèi)存中讀取數(shù)據(jù) load (載入):將主內(nèi)存中讀取到的數(shù)據(jù)寫入到本地(工作)內(nèi)存中 user(使用):從本地內(nèi)存中讀取數(shù)據(jù)給線程使用來計(jì)算 assign(賦值):線程將計(jì)算好的值重新賦值到工作內(nèi)存中 store(存儲):將本地內(nèi)存的數(shù)據(jù)存儲到主內(nèi)存中 write(寫入):將stroe過來的變量值賦值給主內(nèi)存中的變量,重新賦值。大概講一下流程:
在線程B讀取initFlag變量后,重新賦值true給變量,此時,因?yàn)榧恿藇olatile修飾,所以會馬上將值寫入到主內(nèi)存中修改變量中的值,此時因?yàn)橛幸粋€cpu總線嗅探機(jī)制會監(jiān)聽到主內(nèi)存的變量值發(fā)生改變了,會把本地內(nèi)存的中initFlag變量設(shè)置了失效,重新讀取一邊主內(nèi)存的新值,就可以達(dá)到解決變量可見性問題。這是它第一個保證可見性的關(guān)鍵。
之前我們也有提到他如果發(fā)生指令重排序了,那是不是也不能讀取到最新的值呢。答案是不會的呢。
因?yàn)楸籿olatile修飾的話,它會禁止指令重排序。那它主要是依靠什么指令重排序呢?它是通過內(nèi)存屏障來實(shí)現(xiàn)的。什么是內(nèi)存屏障?硬件層面,內(nèi)存屏障分兩種:讀屏障(Load Barrier)和寫屏障(Store Barrier)。內(nèi)存屏障有兩個作用:
阻止屏障兩側(cè)的指令重排序; 強(qiáng)制把寫緩沖區(qū)/高速緩存中的臟數(shù)據(jù)等寫回主內(nèi)存,或者讓緩存中相應(yīng)的數(shù)據(jù)失效。而編譯器在生成字節(jié)碼時,會在指令序列中插入內(nèi)存屏障來禁止指令重排序。這樣保證了任何程序中都能得到正確的volatile內(nèi)存語義。這個策略是:
在每個volatile寫操作前插入一個StoreStore屏障; 在每個volatile寫操作后插入一個StoreLoad屏障; 在每個volatile讀操作后插入一個LoadLoad屏障; 在每個volatile讀操作后再插入一個LoadStore屏障。看一下示意圖:
volatile作用:
volatile可以保證內(nèi)存可見性且禁止重排序。 volatile不具備保證原子性,而鎖可以保證整個臨界區(qū)代碼的執(zhí)行具有原子性。所以而鎖可以保證整個臨界區(qū)代碼的執(zhí)行具有原子性。所以在功能上,鎖比volatile更強(qiáng)大;在性能上,volatile更有優(yōu)勢。2. 悲觀鎖和樂觀鎖可以講下你的理解嗎?其實(shí)聽名字,我們就應(yīng)該有個概念:
悲觀對應(yīng)著我們生活中的人,悲觀的人一般看待事物都會相對消極負(fù)能量點(diǎn),會盡可能往壞處去想的。這也是對應(yīng)著MyGirl,她其實(shí)是一個也不能說算是悲觀的人,只能說看待事物可能會更往深入,更壞的一方面的去思考。
這其實(shí)跟我很互補(bǔ),因?yàn)樗闶莻€樂天派吧,而樂觀對應(yīng)著我們生活中的人,樂觀的人一般看待事物都會相對積極正能量,會盡可能往好處去想的。我其實(shí)對待生活的方方面面可能會更樂觀點(diǎn),但有時帶來的一些壞處也是難以估計(jì)的。
所以說這兩者不能說誰好誰壞,只能對應(yīng)著場景選擇對應(yīng)的方法。
悲觀鎖:
MyGilr這個人呢,她總是會假設(shè)一種最壞的情況。比如,她每次要去拿數(shù)據(jù)的同時,認(rèn)為別人也會來修改數(shù)據(jù)跟她作對,所以每次在拿數(shù)據(jù)的時候她都會上鎖,堵上一個界限,這樣別人想拿這個數(shù)據(jù)就只能等待她出去解鎖成功后,直到它拿到鎖。
在Java中,synchronized和ReentrantLock等獨(dú)占鎖就是悲觀鎖思想的實(shí)現(xiàn)。而在數(shù)據(jù)庫里邊就用到了很多這種鎖機(jī)制,比如行鎖,表鎖等,讀鎖,寫鎖等,都是在做操作之前先上鎖。
樂觀鎖:
我這個人呢,總是會假設(shè)一種最好的情況。比如, 我每次要去拿數(shù)據(jù)的同時,認(rèn)為別人絕對不會來修改數(shù)據(jù)滴,所以每次拿數(shù)據(jù)的時候都不會上鎖。但是人還是要點(diǎn)防備心里的,不是嗎?所以在更新的時候會判斷一下在此期間別人有沒有去更新過這個數(shù)據(jù)。
而常見的有CAS算法+版本號實(shí)現(xiàn)。樂觀鎖適用于多讀的應(yīng)用類型,這樣可以提高吞吐量。
在Java中,像原子類就是使用了樂觀鎖的一種實(shí)現(xiàn)方式CAS實(shí)現(xiàn)的。而在數(shù)據(jù)庫提供的類似于write_condition機(jī)制,其實(shí)都是提供的樂觀鎖。
兩者對應(yīng)的場景的區(qū)別:
樂觀鎖多用于讀多寫少的環(huán)境,避免頻繁加鎖影響性能,加大了系統(tǒng)的整個吞吐量;而悲觀鎖多用于寫多讀少的環(huán)境,避免頻繁失敗和重試影響性能。
3. 你還知道什么其他的鎖嗎?可重入鎖和非可重入鎖:
所謂重入鎖又名遞歸鎖,顧名思義。就是支持重新進(jìn)入的鎖,也就是說這個鎖支持一個線程對資源重復(fù)加鎖。指在同一個線程在外層方法獲取鎖的時候,在進(jìn)入內(nèi)層方法會自動獲取鎖。不會因?yàn)橹耙呀?jīng)獲取過還沒釋放而阻塞。
在Java中,ReentrantLock和synchronized都是可重入鎖,可重入鎖的還有一個優(yōu)點(diǎn)是可一定程度避免死鎖。
public static void main(String[] args) { doOne();}public static synchronized void doOne(){ System.out.println('執(zhí)行第一個任務(wù)'); try {Thread.sleep(10); } catch (InterruptedException e) {e.printStackTrace(); } // 執(zhí)行第二個任務(wù) doTwo();}public static synchronized void doTwo(){ System.out.println('執(zhí)行第二個任務(wù)');}
簡單的測試下結(jié)果:
執(zhí)行第一個任務(wù)執(zhí)行第二個任務(wù)
可以驗(yàn)證得到,類中的兩個方法都是被內(nèi)置鎖synchronized修飾的,而在doOne方法去調(diào)用doTwo方法時,因?yàn)槭强芍厝腈i,所以同個線程下可以直接獲得當(dāng)前對象鎖,所以synchronized是可重入鎖。
而如果我們自己在繼承AQS實(shí)現(xiàn)同步器的時候,沒有考慮到占有鎖的線程再次獲取鎖的場景,可能就會導(dǎo)致線程阻塞,那這個就是一個非可重入鎖。
公平鎖和非公平鎖 :
這里的公平,可以按生活上來講,如果你跟你女朋友吵架,你覺得你是正確的,最后的結(jié)果卻你必須得哄你女朋友還得道歉,你信嗎?所以這是公平的嗎?
如果對一個鎖來說,先對鎖獲取請求的線程一定會先被滿足,后對鎖獲取請求的線程后被滿足,那這個鎖就是公平的。反之,那就是不公平的。
公平鎖:
多個線程按照申請鎖的順序來獲取鎖,線程直接進(jìn)入隊(duì)列中排隊(duì),隊(duì)列中的第一個線程才能獲得鎖。公平鎖的優(yōu)點(diǎn)是等待鎖的線程不會餓死。
缺點(diǎn)是整體吞吐效率相對非公平鎖要低,等待隊(duì)列中除第一個線程以外的所有線程都會阻塞,CPU喚醒阻塞線程的開銷比非公平鎖大。
非公平鎖:
多個線程加鎖時直接嘗試獲取鎖,獲取不到才會到等待隊(duì)列的隊(duì)尾等待。但如果此時鎖剛好可用,那么這個線程可以無需阻塞直接獲取到鎖,所以非公平鎖有可能出現(xiàn)后申請鎖的線程先獲取鎖的場景。
非公平鎖的優(yōu)點(diǎn)是可以減少喚起線程的開銷,整體的吞吐效率高,因?yàn)榫€程有幾率不阻塞直接獲得鎖,CPU不必喚醒所有線程。缺點(diǎn)是處于等待隊(duì)列中的線程可能會餓死,或者等很久才會獲得鎖。
在Java中,對于ReentrantLock而言,可以通過構(gòu)造函數(shù)指定該鎖是否是公平鎖,默認(rèn)是非公平鎖。
獨(dú)享鎖和共享鎖:
對于獨(dú)享和共享,這兩個概念應(yīng)該可以見名知意,對于MyGirl喜歡的東西,是碰都碰不得,而對于不喜歡,或者還可以的東西,可以和她共享。
獨(dú)享鎖:
也叫排他鎖,是指該鎖一次只能被一個線程所持有。如果線程B對變量A加上排它鎖后,則其他線程不能再對A加任何類型的鎖。獲得獨(dú)享鎖的線程即能讀數(shù)據(jù)又能修改數(shù)據(jù)。
在Java中,synchronized就是一種獨(dú)享鎖。
共享鎖:
代表該鎖可被多個線程所持有。如果線程B對變量A加上共享鎖后,則其他線程只能對A再加共享鎖,不能加排它鎖。獲得共享鎖的線程只能讀數(shù)據(jù),不能修改數(shù)據(jù)。
總結(jié)這篇文章就到這里了,如果這篇文章對你也有所幫助,希望您能多多關(guān)注好吧啦網(wǎng)的更多內(nèi)容!
相關(guān)文章:
