一魁兼、線程 8 大核心基礎(chǔ)
1. 有多少種實(shí)現(xiàn)線程的方法?
答題思路婉徘,有以下5點(diǎn):
從不同的角度看,會(huì)有不同的答案咐汞。
典型答案是兩種盖呼,分別是實(shí)現(xiàn)Runnable接口和繼承Thread類,然后具體展開說化撕;
但是几晤,我們看原理,其實(shí)Thread類實(shí)現(xiàn)了Runnable接口植阴,并且看Thread類的run方法蟹瘾,會(huì)發(fā)現(xiàn)其實(shí)那兩種本質(zhì)都是一樣的,run方法的代碼如下: 而target就是我們傳入的Runnable接口掠手。
@Overridepublicvoidrun(){if(target !=null) {target.run();}}復(fù)制代碼
法一和法二憾朴,也就是繼承Thread類然后重寫run()和實(shí)現(xiàn)Runnable接口并傳入Thread類
在實(shí)現(xiàn)多線程的本質(zhì)上并沒有區(qū)別,都是最終調(diào)用了start()方法來新建線程惨撇。這兩個(gè)方法的最主要區(qū)別在于run()方法的內(nèi)容來源:
方法一:最終調(diào)用target.run();
方法二:run()整個(gè)都被重寫
然后具體展開說其他方式
還有其他的實(shí)現(xiàn)線程的方法伊脓,例如線程池等,它們也能新建線程,但是細(xì)看源碼报腔,從沒有逃出過本質(zhì)
也就是實(shí)現(xiàn)Runnable接口和繼承Thread類株搔。
結(jié)論: 我們只能通過新建Thread類這一種方式來創(chuàng)建線程,但是類里面的run方法有兩種方式來實(shí)現(xiàn):
是重寫run方法
實(shí)現(xiàn)Runnable接口的run方法纯蛾,然后再把該runnable實(shí)例傳給Thread類纤房。
除此之外,從表面上看線程池翻诉、定時(shí)器等工具類也可以創(chuàng)建線程炮姨,但是它們的本質(zhì)都逃不出剛才所說的范圍。
以上這種描述比直接回答一種碰煌、兩種舒岸、多種都更準(zhǔn)確。
2. 實(shí)現(xiàn)Runnable接口和繼承Thread類哪種方式更好
從代碼架構(gòu)角度
新建線程的損耗
Java不支持雙繼承
方法1(實(shí)現(xiàn)Runnable接口)更好 方法2是不推薦的芦圾,理由如下:
從代碼架構(gòu)角度:具體的任務(wù)(run方法)應(yīng)該和"創(chuàng)建和運(yùn)行線程的機(jī)制(Thread類)" 解耦蛾派,用runnable對象可以實(shí)現(xiàn)解耦。
通過Runnable方式實(shí)現(xiàn)的run方法中的內(nèi)容是具體執(zhí)行的任務(wù)个少,可以讓一個(gè)單獨(dú)任務(wù)類實(shí)現(xiàn)Runnable接口洪乍,然后把對應(yīng)的實(shí)例傳入Thread類就可以。
這樣的話夜焦,同樣的一個(gè)任務(wù)類壳澳,可以傳給不同的Thread,并且任務(wù)類也不負(fù)責(zé)創(chuàng)建線程等工作茫经,是解耦的巷波。
使用繼承Thread的方式的話,那么每次想新建一個(gè)任務(wù)科平,只能新建一個(gè)獨(dú)立的線程褥紫,而這樣做的損耗會(huì)比較大(比如重頭開始創(chuàng)建一個(gè)線程姜性、執(zhí)行完畢以后再銷毀等瞪慧。如果線程的實(shí)際工作內(nèi)容,也就是run()函數(shù)里只是簡單的打印一行文字的話部念,那么可能線程的實(shí)際工作內(nèi)容還不如損耗來的大)弃酌。
如果使用Runnable和線程池,就可以大大減小這樣的損耗儡炼。
繼承Thread類以后妓湘,由于Java語言不支持雙繼承,這樣就無法再繼承其他的類乌询,限制了可擴(kuò)展性榜贴。通常我們優(yōu)先選擇方法1。
方法2(繼承Thread類)也有幾個(gè)小小的好處妹田,但相比于缺點(diǎn)來說唬党,優(yōu)點(diǎn)都不值一提鹃共,比如:
在run()方法內(nèi)獲取當(dāng)前線程可以直接用this,而無須用Thread.currentThread()方法驶拱;
繼承的類的內(nèi)部變量不會(huì)直接共享霜浴,少數(shù)不需要共享變量的場景下使用起來會(huì)更方便。
兩種方法的本質(zhì)對比
方法一和方法二蓝纲,也就是“實(shí)現(xiàn)Rurnab1e接口并傳入Thread類”和“繼承Thread類然后重寫run()”在實(shí)現(xiàn)多線程的本質(zhì)上阴孟,并沒有區(qū)別,都是最終調(diào)用了start()方法來新建線程税迷。
這兩個(gè)方法的最主要區(qū)別在于run()方法的內(nèi)容來源:
實(shí)現(xiàn)Runnable接口: 最終調(diào)用target.run() ;
繼承Thread類:run()整個(gè)都被重寫
@Overridepublicvoidrun(){if(target !=null) {? ? target.run ();? ? }}復(fù)制代碼
3. 一個(gè)線程兩次調(diào)用start()方法會(huì)出現(xiàn)什么情況?為什么?
情況:拋出一個(gè)
IllegalThreadStateException異常永丝。
原因:Thread的start方法一開始會(huì)對線程狀態(tài)threadStatus進(jìn)行判斷。線程未啟動(dòng)時(shí)箭养,threadStatus=0类溢,當(dāng)線程執(zhí)行了start后,threadStatus就被改變了露懒,第二次再執(zhí)行start方法的時(shí)候闯冷,start方法檢測到threadStatus狀態(tài)不對,就拋出了異常
讓我們來看start()的源碼
publicsynchronizedvoidstart(){if(threadstatus !=0)thrownewIllegalThreadstateException();group.add(this) ;? ? boolean started =false;try{? ? start0 ();? ? started =true;? ? }finally{try{if(!started) {group.threadstartFailed (this);? ? ? ? }? ? }catch(Throwable ignore) { }? ? ? }}privatenativevoidstart0();復(fù)制代碼
流程
檢查線程狀態(tài)懈词,只有NEW狀態(tài)下的線程才能繼續(xù)蛇耀,否則會(huì)拋出IllegalThreadStateException (在運(yùn)行中或者已結(jié)束的線程,都不能再次啟動(dòng))
被加入線程組
調(diào)用start0()方法啟動(dòng)線程
注意點(diǎn):
start方法是被synchronized修飾的方法坎弯,可以保證線程安全纺涤。
由JVM創(chuàng)建的main方法線程和system組線程,并不會(huì)通過start來啟動(dòng)抠忘。
解答
從源碼可以看出撩炊,start的時(shí)候會(huì)先檢查線程狀態(tài),只有NEW狀態(tài)下的線程才能繼續(xù)崎脉,否則會(huì)拋出
IllegalThreadStateException拧咳。
既然Thread.start()只能調(diào)用一次。那么線程池是如何實(shí)現(xiàn)線程復(fù)用的呢囚灼?
線程重用的核心是骆膝,線程池對Thread做了包裝,不重復(fù)調(diào)用thread.start()灶体,而是自己有一個(gè)Runnable.run()阅签,run方法里面循環(huán)在跑,跑的過程中不斷檢查我們是否有新加入的子Runnable對象蝎抽,有新的Runnable進(jìn)來的話就調(diào)一下我們的run()政钟,其實(shí)就一個(gè)大run()把其它小run()#1,run()#2,...給串聯(lián)起來了。
同一個(gè)Thread可以執(zhí)行不同的Runnable,主要原因是線程池把線程和Runnable 通過BlockingQueue給解耦了养交,線程可以從BlockingQueue中不斷獲取新的任務(wù)衷戈。
4. 為什么調(diào)用start()方法?
既然start()方法會(huì)調(diào)用run()方法,為什么我們選擇調(diào)用start()方法层坠,而不是直接調(diào)用run()方法呢?
start()才是真正啟動(dòng)一個(gè)線程殖妇,而如果直接調(diào)用run(),那么run只是一個(gè)普通的方法而已破花,和線程的生命周期沒有任何關(guān)系谦趣。直接調(diào)用run()方法就變成了當(dāng)前的線程調(diào)用了run方法而已
5. 如何正確停止一個(gè)線程?
原理:用interrupt來請求線程停止而不是強(qiáng)制,好處是安全座每。
三方配合:想停止線程前鹅,要請求方、被停止方峭梳、子方法被調(diào)用方相互配合才行:
作為被停止方:每次循環(huán)中或者適時(shí)檢查中斷信號(終止?fàn)顟B(tài))舰绘,并且在可能拋出InterrupedException的地方處理該中斷信號;
請求方:發(fā)出中斷信號 調(diào)用interrupt方法;
子方法調(diào)用方(被線程調(diào)用的方法的作者)要注意含有響應(yīng)方法的兩種異常處理方式:
優(yōu)先在方法層面拋出 throws Exception
InterrupedException,或者檢查到中斷信號時(shí)葱椭,再次設(shè)置中斷狀態(tài) Thread.currentThread().interrupt();;
最后再說錯(cuò)誤的方法:stop/suspend已廢棄捂寿,volatile的boolean無法處理長時(shí)間阻塞的情況
6. 如何處理不可中斷的阻塞
例如搶鎖時(shí)ReentrantLock.lock()或者Socket I/O時(shí)無法響應(yīng)中斷,那應(yīng)該怎么計(jì)該線程停止呢?
A. 用interrupt方法來請求停止線程 B. 不可中斷的阻塞無法處理 C. 根據(jù)不同的類調(diào)用不同的方法 解答:選 C
如果線程阻塞是由于調(diào)用了 wait()孵运,sleep()或join()方法秦陋,你可以中斷線程,通過拋出InterruptedException異常來喚醒該線程治笨。
但是對于不能響應(yīng)InterruptedException的阻塞驳概,很遺憾,并沒有一個(gè)通用的解決方案旷赖。
但是我們可以利用特定的其它的可以響應(yīng)中斷的方法顺又,比如
ReentrantLock.lockInterruptibly(),比如關(guān)閉套接字使線程立即返回等方法來達(dá)到目的等孵。
答案有很多種稚照,因?yàn)橛泻芏嘣驎?huì)造成線程阻塞,所以針對不同情況流济,喚起的方法也不同锐锣。
總結(jié)就是說如果不支持響應(yīng)中斷腌闯,就要用特定方法來喚起绳瘟,沒有萬能藥。
可以為了響應(yīng)中斷而拋出interruptedException的常見方法列表總結(jié):(了解)
Object.wait() / wait(long) / wait(long,int)Thread.sleep(long) / sleep(long,int)Thread.join() /join(long) /join(long,int)java.util.concurrent.BlockingQueue.take()/ put(E)java.util.concurrent.locks.Lock.lockInterruptibly()java.util.concurrent.CountDownLatch.await()java.util.concurrent.CyclicBarrier.await()java.util.concurrent.Exchanger.exchange(v)java.nio.channels.InterruptibleChannel//相關(guān)方法java.nio.channels.Selector//的相關(guān)方法復(fù)制代碼
7. 線程有哪幾種狀態(tài)?生命周期是什么?
6 種狀態(tài)
NEW姿骏、RUNNABLE糖声、BLOCKED、WAITING、TIMED_ WAITING蘸泻、TERMINATED
轉(zhuǎn)換關(guān)系
新創(chuàng)建只能往下走到可運(yùn)行琉苇,而不能直接跳躍到其他狀態(tài);
線程生命周期不可回頭:一旦到了可運(yùn)行就不能回到新創(chuàng)建悦施,一旦被終止并扇,就不能再有任何狀態(tài)的變化了。所以一個(gè)線程只能有一次的“新創(chuàng)建”和“已終止”抡诞。線程是不可以重復(fù)執(zhí)行的穷蛹,當(dāng)它運(yùn)行完了之后便會(huì)結(jié)束,一旦一個(gè)線程進(jìn)入了dead狀態(tài)昼汗,它便不可以重新變回runnable等狀態(tài)肴熏,這個(gè)不可重復(fù)執(zhí)行的性質(zhì)和線程池是一樣的。如果我們還想執(zhí)行該任務(wù)顷窒,可以選擇重新創(chuàng)建一個(gè)線程員蛙吏,而原對象會(huì)被JVM回收。
特殊情況
如果發(fā)生異常鞋吉,可以直接跳到終止TERMINATED狀態(tài)鸦做,不必再遵循路徑,比如可以從WAITING直接到TERMINATED
從0bject.wait()剛被喚醒時(shí)谓着,通常不能立刻搶到monitor鎖馁龟,那就會(huì)從WAITING先進(jìn)入BLOCKED狀態(tài),搶到鎖后再轉(zhuǎn)換到RUNNABLE狀態(tài)漆魔。
圖解狀態(tài) (轉(zhuǎn)化條件)
8. 如何用wait()實(shí)現(xiàn)兩個(gè)線程交替打印 0~100的奇偶數(shù)?
就是有兩個(gè)線程坷檩,一個(gè)線程打印奇數(shù)另一個(gè)打印偶數(shù),它們交替輸出改抡。類似這樣:
偶線程:0
奇線程:1
偶線程:2
...
奇線程:99
偶線程:100
復(fù)制代碼
解題思路
用synchronized實(shí)現(xiàn)
比較容易想的一個(gè)方案是矢炼,要輸出的時(shí)候判斷一下當(dāng)前需要輸出的數(shù)是不是自己要負(fù)責(zé)打印的值,如果是就輸出阿纤,不是就直接釋放鎖句灌。
這個(gè)方法可以滿足題目的要求:兩個(gè)線程,一個(gè)打印奇數(shù)欠拾,一個(gè)打印偶數(shù)胰锌,輪流輸出。但只是用了一個(gè)討巧的方式避開了線程交替獲取鎖的需求藐窄,明顯沒有答到面試官想考察的考點(diǎn)上资昧。而且效率較低,如果同一個(gè)線程一直搶到鎖荆忍,而另一個(gè)線程一直沒有拿到格带,就會(huì)導(dǎo)致線程做很多無謂的空轉(zhuǎn)撤缴。那么有沒有更好的解決方案,讓兩個(gè)線程嚴(yán)格地交替獲取到鎖呢?
WaitNotifyPrintOddEvenSyn類
/**
*? 描述: 兩個(gè)線程交替打印0~100的奇偶數(shù)叽唱,用synchronized關(guān)鍵字實(shí)現(xiàn)
*/publicclassWaitNotifyPrintOddEvenSyn{privatestaticintcount;privatestaticfinal Objectlock=newObject();// 新建2個(gè)線程屈呕, 第一個(gè)只處理偶數(shù),第二個(gè)只處理奇數(shù)(用位運(yùn)算)棺亭,用synchronized來通信publicstaticvoidmain(String[] args){newThread(newRunnable() {? ? ? ? ? ? @Overridepublicvoidrun(){while(count <100) {? ? ? ? ? ? ? ? ? ? synchronized (lock) {if((count &1) ==0) {? ? ? ? ? ? ? ? ? ? ? ? ? ? System.out.println(Thread.currentThread().getName() +":"+ count++);? ? ? ? ? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? }? ? ? ? ? ? }? ? ? ? },"偶線程").start();newThread(newRunnable() {? ? ? ? ? ? @Overridepublicvoidrun(){while(count <100) {? ? ? ? ? ? ? ? ? ? synchronized (lock) {if((count &1) ==1) {? ? ? ? ? ? ? ? ? ? ? ? ? ? System.out.println(Thread.currentThread().getName() +":"+ count++);? ? ? ? ? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? }? ? ? ? ? ? }? ? ? ? },"奇線程").start();? ? }}復(fù)制代碼
注意:這里的synchronized鎖對象不應(yīng)該用count變量虎眨,因?yàn)樵撟兞繄?zhí)行了count++之后,count所指向的對象地址已經(jīng)變了镶摘,詳見問答區(qū):
coding.imooc.com/learn/quest…
拓展閱讀synchronized期間鎖對象不應(yīng)改變:
stackoverflow.com/questions/6…
更好的方法: wait/notify
交替獲取鎖的方案:這種實(shí)現(xiàn)方式的原理就是線程1打印之后喚醒線程2专甩,然后讓出鎖,自己進(jìn)入休眠狀態(tài)钉稍。
因?yàn)檫M(jìn)入了休眠狀態(tài)就不會(huì)與線程2搶鎖涤躲,此時(shí)只有線程2在獲取鎖,所以線程2必然會(huì)拿到鎖贡未。
線程2以同樣的邏輯執(zhí)行种樱,喚醒線程1并讓出自己持有的鎖,自己進(jìn)入休眠狀態(tài)俊卤。
這樣來來回回嫩挤,持續(xù)執(zhí)行直到任務(wù)完。
WaitNotifyPrintOddEvenWait類
/**
* 描述:兩個(gè)線程交替打印0~100的奇偶數(shù)消恍,用wait和notify
*/publicclassWaitNotifyPrintOddEveWait{privatestaticintcount =0;privatestaticfinal Objectlock=newObject();publicstaticvoidmain(String[] args){newThread(newTurningRunner(),"偶線程").start();newThread(newTurningRunner(),"奇線程").start();? ? }// 1. 拿到鎖就打印// 2. 打印完岂昭,喚醒其他線程,自己就休眠staticclassTurningRunnerimplementsRunnable{? ? ? ? @Overridepublicvoidrun(){while(count <=100) {? ? ? ? ? ? ? ? synchronized (lock) {// 拿到鎖就打印System.out.println(Thread.currentThread().getName() +":"+ count++);lock.notify();if(count <=100) {try{// 如果任務(wù)還沒結(jié)束狠怨,就讓出當(dāng)前的鎖约啊,并休眠lock.wait();? ? ? ? ? ? ? ? ? ? ? ? }catch(InterruptedException e) {? ? ? ? ? ? ? ? ? ? ? ? ? ? e.printStackTrace();? ? ? ? ? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? }? ? ? ? ? ? }? ? ? ? }? ? }}復(fù)制代碼
9. 如何用wait實(shí)現(xiàn)生產(chǎn)者模式?
為什么要使用生產(chǎn)者和消費(fèi)者模式
在線程世界里,生產(chǎn)者就是生產(chǎn)數(shù)據(jù)的線程佣赖,消費(fèi)者就是消費(fèi)數(shù)據(jù)的線程恰矩。在多線程開發(fā)當(dāng)中,如果生產(chǎn)者處理速度很快憎蛤,而消費(fèi)者處理速度很慢外傅,那么生產(chǎn)者就必須等待消費(fèi)者處理完,才能繼續(xù)生產(chǎn)數(shù)據(jù)俩檬。同樣的道理萎胰,如果消費(fèi)者的處理能力大于生產(chǎn)者,那么消費(fèi)者就必須等待生產(chǎn)者棚辽。為了解決這個(gè)問題于是引入了生產(chǎn)者和消費(fèi)者模式技竟。
什么是生產(chǎn)者消費(fèi)者模式
生產(chǎn)者消費(fèi)者模式(后面簡稱為生產(chǎn)者模式)是非常經(jīng)典的設(shè)計(jì)模式之一,而實(shí)現(xiàn)生產(chǎn)者模式是 wait/notify的典型用途之一晚胡。
我們將在本小節(jié)中學(xué)習(xí)生產(chǎn)者模式灵奖,以及如何用wait/notify來實(shí)現(xiàn)它嚼沿。
面到過用wait/notify手寫生產(chǎn)者模式的問題估盘,這很重要的一個(gè)知識點(diǎn)瓷患,很基礎(chǔ),但是可以全面考察候選人的內(nèi)功遣妥。
生產(chǎn)者消費(fèi)者模式是通過一個(gè)容器來解決生產(chǎn)者和消費(fèi)者的強(qiáng)耦合問題擅编。生產(chǎn)者和消費(fèi)者彼此之間不直接通訊,而通過阻塞隊(duì)列來進(jìn)行通訊箫踩,所以生產(chǎn)者生產(chǎn)完數(shù)據(jù)之后不用等待消費(fèi)者處理爱态,直接扔給阻塞隊(duì)列,消費(fèi)者不找生產(chǎn)者要數(shù)據(jù)境钟,而是直接從阻塞隊(duì)列里取锦担,阻塞隊(duì)列就相當(dāng)于一個(gè)緩沖區(qū),平衡了生產(chǎn)者和消費(fèi)者的處理能力慨削。
特點(diǎn)
生產(chǎn)者僅在倉庫未滿的時(shí)候生產(chǎn)洞渔,如果倉庫滿了就停止生產(chǎn)。
消費(fèi)者僅在倉庫有產(chǎn)品時(shí)才能消費(fèi)缚态,如果倉庫里面沒產(chǎn)品磁椒,就等待。
當(dāng)消費(fèi)者發(fā)現(xiàn)倉庫沒產(chǎn)品了玫芦,會(huì)通知生產(chǎn)者去生產(chǎn)浆熔。
當(dāng)生產(chǎn)者生產(chǎn)出可了產(chǎn)品,會(huì)通知等待的消費(fèi)者去消費(fèi)。
本次是用wait/notify實(shí)現(xiàn),后面還會(huì)要求用Condition和BlockingQueue實(shí)現(xiàn)生產(chǎn)者消費(fèi)者模式头岔,原理都是一樣的亲雪。
示意圖
代碼
這里在Storage初始化的時(shí)候,配置了容量若锁,所以滿了就會(huì)阻塞。
/**
* 描述:用wait/notify來實(shí)現(xiàn)生產(chǎn)者消費(fèi)者模式
*/publicclassProducerConsumerModel{publicstaticvoidmain(String[] args){? ? ? ? EventStorage eventStorage =newEventStorage();? ? ? ? Producer producer =newProducer(eventStorage);? ? ? ? Consumer consumer =newConsumer(eventStorage);newThread(producer).start();newThread(consumer).start();? ? }}classProducerimplementsRunnable{privateEventStorage storage;publicProducer(
? ? ? ? ? ? EventStorage storage){this.storage = storage;? ? }@Overridepublicvoidrun(){for(inti =0; i <100; i++) {? ? ? ? ? ? storage.put();? ? ? ? }? ? }}classConsumerimplementsRunnable{privateEventStorage storage;publicConsumer(
? ? ? ? ? ? EventStorage storage){this.storage = storage;? ? }@Overridepublicvoidrun(){for(inti =0; i <100; i++) {? ? ? ? ? ? storage.take();? ? ? ? }? ? }}classEventStorage{privateintmaxSize;privateLinkedList storage;publicEventStorage(){? ? ? ? maxSize =10;? ? ? ? storage =newLinkedList<>();? ? }publicsynchronizedvoidput(){while(storage.size() == maxSize) {try{? ? ? ? ? ? ? ? wait();? ? ? ? ? ? }catch(InterruptedException e) {? ? ? ? ? ? ? ? e.printStackTrace();? ? ? ? ? ? }? ? ? ? }? ? ? ? storage.add(newDate());? ? ? ? System.out.println("倉庫里有了"+ storage.size() +"個(gè)產(chǎn)品。");? ? ? ? notify();? ? }publicsynchronizedvoidtake(){while(storage.size() ==0) {try{? ? ? ? ? ? ? ? wait();? ? ? ? ? ? }catch(InterruptedException e) {? ? ? ? ? ? ? ? e.printStackTrace();? ? ? ? ? ? }? ? ? ? }? ? ? ? System.out.println("拿到了"+ storage.poll() +"邓萨,現(xiàn)在倉庫還剩下"+ storage.size());? ? ? ? notify();? ? }}復(fù)制代碼
10. 為什么wait必須在同步代碼塊中使用?
我們反過來想,如果不要求wait必須在同步塊里面菊卷,而是可以在之外調(diào)用的話缔恳,那么就會(huì)有以下代碼:
classBlockingQueue{? ? Queue buffer =newLinkedList();publicvoidgive(String data){? ? ? ? buffer.add(data);? ? ? ? notify();// Since someone may be waiting in take!}publicStringtake() throws InterruptedException{while(buffer.isEmpty()) {// 不能用if,因?yàn)闉榱朔乐固摷賳拘褀ait();? ? ? ? }returnbuffer.remove();? ? }}復(fù)制代碼
那么可能發(fā)生如下的錯(cuò)誤:
消費(fèi)者線程調(diào)用take()并看到了buffer.isEmpty()洁闰。
在消費(fèi)者線程繼續(xù)wait()之前歉甚,生產(chǎn)者線程調(diào)用一個(gè)完整的give(),也就是buffer.add(data)和notify()
消費(fèi)者線程現(xiàn)在調(diào)用wait()扑眉,但是錯(cuò)過了剛才的notify() (注意纸泄,由于錯(cuò)誤的條件判斷赖钞,導(dǎo)致wait調(diào)用在notify之后,這是關(guān)鍵)
如果運(yùn)氣不好聘裁,即使有可用的數(shù)據(jù)雪营,但是沒有更多生產(chǎn)者生產(chǎn)的話,那么消費(fèi)者會(huì)陷入wait的無限期等待衡便。造成死鎖献起。
一旦你理解了這個(gè)問題,解決方案是顯而易見的:
synchronized用來確保notify永遠(yuǎn)不會(huì)在isEmpty和wait之間被調(diào)用镣陕。當(dāng)執(zhí)行wait之前因?yàn)闆]有同步代碼塊的保護(hù)谴餐,可能會(huì)切到notify。這不是我們的初衷呆抑。并且notfiy都執(zhí)行完畢了岂嗓,這就會(huì)導(dǎo)致wait的線程沒有人來喚醒它。編程永久等待或者死鎖
解決這個(gè)問題的方法就是:
總是讓give/notify和take/wait為原子操作鹊碍。
也就是說wait/notify是線程之間的通信厌殉,他們存在競態(tài),我們必須保證在滿足條件的情況下才進(jìn)行wait妹萨。
換句話說年枕,如果不加鎖的話,那么wait被調(diào)用的時(shí)候可能wait的條件已經(jīng)不滿足了(如上述)乎完。
由于錯(cuò)誤的條件下進(jìn)行了wait熏兄,那么就有可能永遠(yuǎn)不會(huì)被notify到,所以我們需要強(qiáng)制wait/notify在synchronized中
11. 為什么線程通信的方法wait(), notify()和notifyAll()被定義在Object類里?而sleep定義在Thread類里?
每個(gè)對象都可上鎖树姨,由于wait摩桶,notify和notifyAll都是鎖級別的操作,所以把他們定義在Object類中帽揪,因?yàn)殒i屬于對象硝清。
Java的每個(gè)對象中都有一個(gè)鎖(monitor,也可以成為監(jiān)視器) 并且wait()转晰,notify()等方法用于等待對象的鎖或者通知其他線程對象的監(jiān)視器可用芦拿。
在Java的線程中并沒有可供任何對象使用的鎖和同步器。
這就是為什么這些方法是Object類的一部分查邢,這樣Java的每一個(gè)類都有用于線程間通信的基本方法蔗崎。
一個(gè)很明顯的原因是JAVA提供的鎖是對象級的而不是線程級的,每個(gè)對象都有鎖扰藕,通過線程獲得缓苛。
如果線程需要等待某些鎖那么調(diào)用對象中的wait()方法就有意義了。如果wait()方法定義在Thread類中邓深,線程正在等待的是哪個(gè)鎖就不明顯了未桥。
每個(gè)對象都擁有monitor(即鎖)笔刹,所以讓當(dāng)前線程等待某個(gè)對象的鎖,當(dāng)然應(yīng)該通過這個(gè)對象來操作了冬耿。而不是用當(dāng)前線程來操作舌菜,因?yàn)楫?dāng)前線程可能會(huì)等待多個(gè)線程的鎖,如果通過線程來操作淆党,就非常復(fù)雜了酷师。
注:如果對monitor不熟悉讶凉,可以看我的免費(fèi)課:Java高并發(fā)之魂:synchronized深度解析www.imooc.com/learn/1086
這兩者的施加者是有本質(zhì)區(qū)別的.
sleep()是讓某個(gè)線程暫停運(yùn)行一段時(shí)間,其控制范圍是由當(dāng)前線程決定,也就是說,在線程里面決定.好比如說,我要做的事情是 "點(diǎn)火->燒水->煮面",而當(dāng)我點(diǎn)完火之后我不立即燒水,我要休息一段時(shí)間再燒.對于運(yùn)行的主動(dòng)權(quán)是由我的流程來控制.
而wait(),首先,這是由某個(gè)確定的對象來調(diào)用的,將這個(gè)對象理解成一個(gè)傳話的人,當(dāng)這個(gè)人在某個(gè)線程里面說"暫停!",也是 thisOBJ.wait(),這里的暫停是阻塞,還是"點(diǎn)火->燒水->煮飯",thisOBJ就好比一個(gè)監(jiān)督我的人站在我旁邊,本來該線 程應(yīng)該執(zhí)行1后執(zhí)行2,再執(zhí)行3,而在2處被那個(gè)對象喊暫停,那么我就會(huì)一直等在這里而不執(zhí)行3,但正個(gè)流程并沒有結(jié)束,我一直想去煮飯,但還沒被允許, 直到那個(gè)對象在某個(gè)地方說"通知暫停的線程啟動(dòng)!",也就是thisOBJ.notify()的時(shí)候,那么我就可以煮飯了,這個(gè)被暫停的線程就會(huì)從暫停處 繼續(xù)執(zhí)行.
一個(gè)很明顯的原因是JAVA提供的鎖是對象級的而不是線程級的染乌,每個(gè)對象都有鎖,通過線程獲得懂讯。如果線程需要等待某些鎖那么調(diào)用對象中的wait()方法就有意義了荷憋。如果wait()方法定義在Thread類中,線程正在等待的是哪個(gè)鎖就不明顯了褐望。
簡單的說勒庄,由于wait,notify和notifyAll都是鎖級別的操作瘫里,所以把他們定義在Object類中因?yàn)殒i屬于對象实蔽。
其實(shí)兩者都可以讓線程暫停一段時(shí)間,但是本質(zhì)的區(qū)別是一個(gè)線程的運(yùn)行狀態(tài)控制,一個(gè)是線程之間的通訊的問題
12. wait方法是屬于0bject對象的,那調(diào)用Thread.wait會(huì)怎么樣?
這里就把Thread當(dāng)成是一個(gè)普通的類谨读,和object沒有區(qū)別局装。
但是這樣會(huì)有一個(gè)問題,那就是線程退出的時(shí)候會(huì)自動(dòng)notify()劳殖,這會(huì)讓我們自己設(shè)計(jì)的喚醒流程受到極大的干擾铐尚,所以十分不推薦調(diào)用Thread類的wait()。因?yàn)檫@會(huì)影響到系統(tǒng)API的正常運(yùn)行哆姻,或者被系統(tǒng)API影響到宣增。
13. 如何選擇用notify還是notifyA11?
0bject.notify()可能導(dǎo)致信號丟失這樣的正確性問題,而Object.notifyAll()雖然效率不太高(把不需要喚醒的等待線程也給喚醒了)﹐但是其在正確性方面有保障矛缨。因此實(shí)現(xiàn)通知的一種比較流行的保守性方法是優(yōu)先使用0bject.notifyAll()以保障正確性爹脾,只有在有證據(jù)表明使用0bject.notify()足夠的情況下才使用0bject.notify()——Object.notify()只有在下列條件全部滿足的情況下才能夠用于替代notifyAll方法。
條件1:一次通知僅需要喚醒至多一個(gè)線程箕昭。這一點(diǎn)容易理解灵妨,但是光滿足這一點(diǎn)還不足以用0bject.notify()去替代0bject.notifyAll()。在不同的等待線程可能使用不同的保護(hù)條件的情況下盟广,0bject.notify()喚醒的一個(gè)任意線程可能并不是我們需要喚醒的那一個(gè)(種)線程闷串。因此,這個(gè)問題還需要通過滿足條件2來排除筋量。
條件2:相應(yīng)對象的等待集中僅包含同質(zhì)等待線程烹吵。所謂同質(zhì)等待線程指這些線程使用同一個(gè)保護(hù)條件碉熄,并且這些線程在Object.wait()調(diào)用返回之后的處理邏輯一致。最為典型的同質(zhì)線程是使用同一個(gè)Runnable接口實(shí)例創(chuàng)建的不同線程(實(shí)例)或者從同一個(gè)Thread子類的new出來的多個(gè)實(shí)例肋拔。
注意: 0bject.notify()喚醒的是其所屬對象上的一個(gè)任意等待線程锈津。0bject.notify()本身在喚醒線程時(shí)是不考慮保護(hù)條件的。0bject.notifyAll()方法喚醒的是其所屬對象上的所有等待線程凉蜂。使用0bject.notify()替代0bject.notifyAll()時(shí)需要確保以下兩個(gè)條件同時(shí)得以滿足:
一次通知僅需要喚醒至多一個(gè)線程琼梆。
相應(yīng)對象上的所有等待線程都是同質(zhì)等待線程。
參考:
www.reibang.com/p/5834de089…
14. notifyAll之后所有的線程都會(huì)再次搶奪鎖窿吩,如果某線程搶奪失敗怎么辦?
繼續(xù)等待茎杂,不會(huì)有其他動(dòng)作,就和等待synchronized的monitor一樣纫雁。
15. 用suspend()和resume()來阻塞線程可以嗎?為什么?
suspend()和 resume()已經(jīng)不推薦使用煌往,功能類似于wait和notify,但是不釋放鎖轧邪,并且容易引起死鎖刽脖。
16. wait/notify、sleep的異同(方法屬于哪個(gè)對象?線程狀態(tài)怎么切換?)
相同點(diǎn)
他們都可以讓線程阻塞 對應(yīng)線程狀態(tài)是Waiting或Time_Waiting忌愚。
wait和sleep方法都可以響應(yīng)中斷Thread.interrupt():在等待的過程中如果收到中斷信號曲管,都可以進(jìn)行響應(yīng),并拋出InterruptedException
不同點(diǎn)
wait方法必須在synchronized保護(hù)的代碼中使用硕糊,而sleep方法并沒有這個(gè)要求
在同步代碼中執(zhí)行sleep方法時(shí)院水,并不會(huì)釋放monitor鎖,但執(zhí)行wait方法時(shí)會(huì)主動(dòng)釋放monitor鎖
sleep方法中要求必須定義一個(gè)時(shí)間癌幕,時(shí)間到期后會(huì)主動(dòng)恢復(fù)衙耕,而對于沒有參數(shù)wait方法而言,意味著永久等待勺远,直到被中斷或者喚醒才能恢復(fù)橙喘,他并不會(huì)主動(dòng)恢復(fù)
wait/notify是Object方法,而sleep是Thread類的方法
sleep必須捕獲異常胶逢,而wait厅瞎,notify和notifyAll不需要捕獲異常
17. 在join期間,線程處于哪種線程狀態(tài)?
有書本說是Blocked狀態(tài)(Java高并發(fā)編程詳解多線程與架構(gòu)設(shè)計(jì)3.8.1)初坠,但是這是不對的和簸。
我們自己動(dòng)手來實(shí)驗(yàn),實(shí)踐出真知碟刺,代碼不會(huì)騙人锁保。
答案是Waiting,那么為什么不是Timed_waiting呢,因?yàn)閖oin的時(shí)候無法預(yù)料實(shí)際等待時(shí)間是多少爽柒。
18. yield和sleep區(qū)別?
yield和sleep區(qū)別是吴菠,sleep期間線程調(diào)度器不會(huì)去調(diào)度該線程,而yield方法時(shí)只是讓線程釋放出自己的CPU時(shí)間片浩村,線程依然處于就緒狀態(tài)做葵,隨時(shí)可能再次被調(diào)度。
19. 守護(hù)線程和普通線程的區(qū)別?
User和Daemon兩者幾乎沒有區(qū)別心墅,唯一的不同之處就在于虛擬機(jī)的離開:如果 User Thread已經(jīng)全部退出運(yùn)行了酿矢,只剩下Daemon Thread存在了,虛擬機(jī)也就退出了怎燥,這是因?yàn)闆]有了“被守護(hù)者”瘫筐,Daemon 也就沒有工作可做了,也就沒有繼續(xù)運(yùn)行程序的必要了刺覆。
20. 我們是否需要給線程設(shè)置為守護(hù)線程?
我們通常不應(yīng)把自己的線程設(shè)置為守護(hù)線程严肪,因?yàn)樵O(shè)置為守護(hù)線程是很危險(xiǎn)的史煎。
比如線程正在訪問如文件谦屑、數(shù)據(jù)庫的時(shí)候,所有用戶線程都結(jié)束了篇梭,那么守護(hù)線程會(huì)在任何時(shí)候甚至在一個(gè)操作的中間發(fā)生中斷氢橙,所以守護(hù)線程永遠(yuǎn)不應(yīng)該去訪問固有資源。
參考資料: blog.csdn.net/liuxiao7238…
21. 為什么程序設(shè)計(jì)不應(yīng)依賴于線程優(yōu)先級?
由于優(yōu)先級最終是由線程調(diào)度器來決定調(diào)度方案的恬偷,所以優(yōu)先級高并不能保證就一定比優(yōu)先級低的先運(yùn)行;并且如果優(yōu)先級設(shè)置得不合適悍手,可能會(huì)導(dǎo)致線程饑餓等問題(優(yōu)先級低的線程始終得不到運(yùn)行),所以通常而言袍患,我們不必要設(shè)置線程的優(yōu)先級屬性坦康,保持默認(rèn)的優(yōu)先級就可以。
給線程設(shè)置的優(yōu)先級的意圖是希望高優(yōu)先級的線程被優(yōu)先執(zhí)行诡延,但是線程優(yōu)先級的執(zhí)行情況是高度依賴于操作系統(tǒng)的滞欠,Java的10個(gè)線程的優(yōu)先級會(huì)被映射到操作系統(tǒng)的優(yōu)先級上,不同的操作系統(tǒng)的優(yōu)先級個(gè)數(shù)也許更多肆良,也許更少筛璧。 例如, Windows 有7 個(gè)優(yōu)先級別惹恃,對應(yīng)關(guān)系如下:
但是在Oracle 為Linux 提供的Java 虛擬機(jī)中夭谤,線程的優(yōu)先級被忽略——所有線程具有相同的優(yōu)先級;
Solaris中有2的32次這么多個(gè)優(yōu)先級級別巫糙。
所以可以看出朗儒,“優(yōu)先級”這個(gè)屬性,會(huì)隨著操作系統(tǒng)的不同而變化,這是很不可靠的醉锄,剛才僅僅是優(yōu)先級不可靠的第一個(gè)原因
還有第二個(gè)原因:優(yōu)先級可能會(huì)被系統(tǒng)自行改變疲牵。例如,在Windows系統(tǒng)中存在一個(gè)稱為“優(yōu)先級推進(jìn)器”(Priority Boosting榆鼠,當(dāng)然它可以被關(guān)閉掉)的功能纲爸,它的大致作用就是當(dāng)系統(tǒng)發(fā)現(xiàn)一個(gè)線程執(zhí)行得特別“勤奮努力”的話,可能會(huì)越過線程優(yōu)先級去為它分配執(zhí)行時(shí)間妆够。因此识啦,我們不能在程序中通過優(yōu)先級來完全準(zhǔn)確地判斷一組狀態(tài)都為Ready的線程將會(huì)先執(zhí)行哪一個(gè)。
因此我們不應(yīng)該把程序的正確性依賴于優(yōu)先級神妹。
22. Java異常體系圖
需要注意的是,所有的異常都是由 Throwable繼承而來,但在下一層立即分解為兩個(gè)分支:Eror和Exception颓哮。
Eror類層次結(jié)構(gòu)描述了Java運(yùn)行時(shí)系統(tǒng)的內(nèi)部錯(cuò)誤和資源耗盡錯(cuò)誤。應(yīng)用程序不應(yīng)該拋出這種類型的對象鸵荠。如果出現(xiàn)了這樣的內(nèi)部錯(cuò)誤,除了通告給用戶,并盡力使程序安全地終止之外,再也無能為力了冕茅。這種情況很少出現(xiàn)。
在設(shè)計(jì)Java程序時(shí),需要關(guān)注 Exception層次結(jié)構(gòu)這個(gè)層次結(jié)構(gòu)又分解為兩個(gè)分支:
一個(gè)分支派生于Runtime Exception;
另一個(gè)分支包含其他異常蛹找。
劃分兩個(gè)分支的規(guī)則是:由程序錯(cuò)誤導(dǎo)致的異常屬于 RuntimeException;而程序本身沒有問題,但由于像I/O錯(cuò)誤這類問題導(dǎo)致的異常屬于其他異常姨伤。
派生于 RuntimeException的異常包含下面幾種情況:
錯(cuò)誤的類型轉(zhuǎn)換。
數(shù)組訪問越界庸疾。
訪問nul指針乍楚。等。届慈。徒溪。
不是派生于 RuntimeException的異常包括:
試圖在文件尾部后面讀取數(shù)據(jù)。
試圖打開一個(gè)不存在的文件金顿。
試圖根據(jù)給定的字符串查找 Class對象,而這個(gè)字符串表示的類并不存在臊泌。等。揍拆。渠概。
如果出現(xiàn) RuntimeException異常,那么就一定是你的問題”是一條相當(dāng)有道理的規(guī)則。
應(yīng)該通過檢測數(shù)組下標(biāo)是否越界來避免 ArraylndexOutOfBounds Exception異常;
應(yīng)該通過在使用變量之前檢測是否為null來杜絕 NullPointerException異常的發(fā)生
但是如何處理不存在的文件呢?難道不能先檢查文件是否存在再打開它嗎?嗯,這個(gè)文件有可能在你檢査它是否存在之前就已經(jīng)被刪除了礁凡。因此,“是否存在”取決于環(huán)境,而不只是取決于你的代碼高氮。
Java語言規(guī)范:
派生于Eror類或 RuntimeException類的所有異常稱為非受查(unchecked)異常
所有其他的異常稱為受查(checked)異常。
這是兩個(gè)很有用的術(shù)語,編譯器將核查是否為所有的受查異常提供了異常處理器顷牌。
ps: RuntimeException這個(gè)名字很容易讓人混淆剪芍。實(shí)際上,現(xiàn)在討論的所有錯(cuò)誤都發(fā)生在運(yùn)行時(shí)。
23. 實(shí)際工作中窟蓝,如何全局處理異常?
給程序統(tǒng)一設(shè)置
先自己實(shí)現(xiàn)UncaughtExceptionHandler接口罪裹,在uncaughtException(Thread t饱普,Throwable e)的實(shí)現(xiàn)上,根據(jù)業(yè)務(wù)需要可以有不同策略状共,最常見的方式是把錯(cuò)誤信息寫入日志套耕,或者重啟線程、或執(zhí)行其他修復(fù)或診斷峡继。
代碼演示:UncaughtExceptionHandler類
/**
* 描述:自己的 MyUncaughtExceptionHanlder
*/publicclassMyUncaughtExceptionHandlerimplementsThread.UncaughtExceptionHandler{privateString name;publicMyUncaughtExceptionHandler(String name){this.name = name;? ? }@OverridepublicvoiduncaughtException(Thread t, Throwable e){? ? ? ? Logger logger = Logger.getAnonymousLogger();? ? ? ? logger.log(Level.WARNING,"線程異常冯袍,終止啦"+ t.getName());? ? ? ? System.out.println(name +"捕獲了異常"+ t.getName() +"異常");? ? }}/**
* 描述:使用剛才自己寫的UncaughtExceptionHandler
*/publicclassUseOwnUncaughtExceptionHandlerimplementsRunnable{publicstaticvoidmain(String[] args)throwsInterruptedException{? ? ? ? Thread.setDefaultUncaughtExceptionHandler(newMyUncaughtExceptionHandler("捕獲器1"));newThread(newUseOwnUncaughtExceptionHandler(),"MyThread-1").start();? ? ? ? Thread.sleep(300);newThread(newUseOwnUncaughtExceptionHandler(),"MyThread-2").start();? ? ? ? Thread.sleep(300);newThread(newUseOwnUncaughtExceptionHandler(),"MyThread-3").start();? ? ? ? Thread.sleep(300);newThread(newUseOwnUncaughtExceptionHandler(),"MyThread-4").start();? ? }@Overridepublicvoidrun(){thrownewRuntimeException();? ? }}復(fù)制代碼
給每個(gè)線程或線程池單獨(dú)設(shè)置
剛才我們是給整個(gè)程序設(shè)置了默認(rèn)的UncaughtExceptionHandler,這是通常的做法碾牌。當(dāng)然康愤,如果業(yè)務(wù)有特殊需求,我們也可以給某個(gè)線程或者線程池指定單獨(dú)的特定的UncaughtExceptionHandler舶吗,這樣可以更精細(xì)化處理征冷。
24. 為什么異常需要全局處理?不處理行不行?
不處理是不行的,因?yàn)榉駝t異常信息會(huì)拋給前端誓琼,這會(huì)讓重要的信息泄露检激,不安全。
只要是未處理異常腹侣,我們返回給前端就是簡單的一句話“意外錯(cuò)誤”叔收,而不應(yīng)該把異常棧信息也告訴前端,否則會(huì)被白帽子筐带、黑客利用今穿。
25. run方法是否可以拋出異常?如果拋出異常,線程的狀態(tài)會(huì)怎么樣?
run方法不能拋出異常伦籍,如果運(yùn)行時(shí)發(fā)生異常,線程會(huì)停止運(yùn)行腮出,狀態(tài)變成Terminated 但是不影響主線程的運(yùn)行帖鸦。
26. 一共有哪幾類線程安全問題?
運(yùn)行結(jié)果錯(cuò)誤
例如a++多線程下出現(xiàn)結(jié)果小于累加次數(shù)。
對象發(fā)布和初始化的時(shí)候的安全問題:
方法返回一個(gè)private對象(private的本意是不讓外部訪問)
還未完成初始化(構(gòu)造函數(shù)沒完全執(zhí)行完畢)就把對象提供給外界胚嘲,比如: 在構(gòu)造函數(shù)中未初始化完畢就this賦值 隱式逸出——注冊監(jiān)聽事件 構(gòu)造函數(shù)中運(yùn)行線程
死鎖等問題
27. 哪些場景需要額外注意線程安全問題作儿?
訪問共享的變量或資源,會(huì)有并發(fā)風(fēng)險(xiǎn)馋劈,比如對象的屬性攻锰、靜態(tài)變量、共享緩存妓雾、數(shù)據(jù)庫等
所有依賴時(shí)序的操作娶吞,即使每一步操作都是線程安全的,還是存在并發(fā)問題: read-modify-write操作:一個(gè)線程讀取了一個(gè)共享數(shù)據(jù)械姻,并在此基礎(chǔ)上更新該數(shù)據(jù)妒蛇,例如 index++ check-then-act操作:一個(gè)線程讀取了一個(gè)共享數(shù)據(jù),并在此基礎(chǔ)上決定其下一個(gè)的操作
不同的數(shù)據(jù)之間存在捆綁關(guān)系的時(shí)候
我們使用其他類的時(shí)候,如果對方?jīng)]有聲明自己是線程安全的绣夺,那么大概率會(huì)存在并發(fā)問題的隱患
28. 為什么多線程會(huì)帶來性能問題吏奸?
體現(xiàn)在兩個(gè)方面:線程的調(diào)度和協(xié)作,這兩個(gè)方面通常相輔相成陶耍,也就是說奋蔚,由于線程需要協(xié)作,所以會(huì)引起調(diào)度:
調(diào)度:上下文切換
什么時(shí)候會(huì)需要線程調(diào)度呢?當(dāng)可運(yùn)行的線程數(shù)超過了CPU核心數(shù)烈钞,那么操作系統(tǒng)就要調(diào)度線程旺拉,以便于讓每個(gè)線程都有運(yùn)行的機(jī)會(huì)。調(diào)度會(huì)引起上下文切換棵磷。
例如當(dāng)某個(gè)線程運(yùn)行Thread.sleep(1000);的時(shí)候蛾狗,線程調(diào)度器就會(huì)讓當(dāng)前這個(gè)線程阻塞,然后往往會(huì)讓另一個(gè)正在等待CPU資源的線程進(jìn)入可運(yùn)行狀態(tài)仪媒,這里會(huì)產(chǎn)生“上下文切換”沉桌,這是一種比較大的開銷,有時(shí)用于上下文切換的時(shí)間甚至比線程執(zhí)行的時(shí)候更長算吩。
通常而言留凭,一次上下文切換所帶來的開銷大約在5000~10000個(gè)時(shí)鐘周期,大約幾微秒偎巢,看似不起眼蔼夜,其實(shí)已經(jīng)是不小的性能損耗了。
什么是上下文压昼?
同學(xué)們?nèi)绻麑Σ僮飨到y(tǒng)這門課熟悉求冷,就比較了解上下文,不過我擔(dān)心小伙伴們可能不熟悉操作系統(tǒng)窍霞,所以我在這里補(bǔ)充下“上下文”的知識:
上下文是指某一時(shí)間點(diǎn) CPU 寄存器和程序計(jì)數(shù)器的內(nèi)容匠题。
寄存器是 CPU 內(nèi)部的數(shù)量較少但是速度很快的內(nèi)存(與之對應(yīng)的是 CPU 外部相對較慢的 RAM 主內(nèi)存)寄存器通過對常用值(通常是運(yùn)算的中間值)的快速訪問來提高計(jì)算機(jī)程序運(yùn)行的速度。
程序計(jì)數(shù)器是一個(gè)專用的寄存器但金,用于表明指令序列中 CPU 正在執(zhí)行的位置韭山,存的值為正在執(zhí)行的指令的位置或者下一個(gè)將要被執(zhí)行的指令的位置,具體依賴于特定的系統(tǒng)冷溃。
稍微詳細(xì)描述一下钱磅,上下文切換可以認(rèn)為是內(nèi)核(操作系統(tǒng)的核心)在 CPU 上對于進(jìn)程(包括線程)進(jìn)行以下的活動(dòng)
(1)掛起一個(gè)進(jìn)程,將這個(gè)進(jìn)程在 CPU 中的狀態(tài)(上下文)存儲(chǔ)于內(nèi)存中的某處
(2)在內(nèi)存中檢索下一個(gè)進(jìn)程的上下文并將其在 CPU 的寄存器中恢復(fù)
(3)跳轉(zhuǎn)到程序計(jì)數(shù)器所指向的位置(即跳轉(zhuǎn)到進(jìn)程被中斷時(shí)的代碼行)似枕,以恢復(fù)該進(jìn)程盖淡。
緩存開銷
除了剛才提到的上下文切換帶來的直接開銷外,還需要考慮到間接帶來的緩存失效的問題菠净。
我們知道程序有很大概率會(huì)訪問剛才訪問過的數(shù)據(jù)禁舷,所以CPU為了加快執(zhí)行速度彪杉,會(huì)根據(jù)不同算法,把常用到的數(shù)據(jù)緩存到CPU內(nèi)牵咙,這樣以后再用到該數(shù)據(jù)時(shí)派近,可以很快使用。
但是現(xiàn)在上下文被切換了洁桌,也就是說渴丸,CPU即將執(zhí)行不同的線程的不同的代碼,那么原本緩存的內(nèi)容有極大概率也沒有價(jià)值了另凌。
這就需要CPU重新緩存谱轨,這導(dǎo)致線程在被調(diào)度運(yùn)行后,一開始的啟動(dòng)速度會(huì)有點(diǎn)慢吠谢。
為此土童,線程調(diào)度器為了避免太頻繁的上下文切換帶來的開銷過大,往往會(huì)給每個(gè)被調(diào)度到的線程設(shè)置一個(gè)“最小執(zhí)行時(shí)間”工坊,這樣就把上下文切換的最小閾值提高献汗,減少上下文切換的次數(shù),從而提高性能(但是當(dāng)然王污,也降低了響應(yīng)性)罢吃。
何時(shí)會(huì)導(dǎo)致密集的上下文切換
如果程序頻繁地競爭鎖,或者由于IO讀寫等原因?qū)е骂l繁阻塞昭齐,那么這個(gè)程序就可能需要更多的上下文切換尿招,這也就導(dǎo)致了更大的開銷。
協(xié)作:內(nèi)存同步
線程之間如果使用共享數(shù)據(jù)阱驾,那么為了避免數(shù)據(jù)混亂就谜,肯定要使用同步手段,為了數(shù)據(jù)的正確性,同步手段往往會(huì)使用禁止編譯器優(yōu)化、使CPU內(nèi)的緩存失效等手段陌兑,這顯然帶來了額外開銷纱新,因?yàn)闇p少了原本可以進(jìn)行的優(yōu)化。
29. 什么是多線程的上下文切換捆愁?
上下文切換可以認(rèn)為是內(nèi)核(操作系統(tǒng)的核心)在 CPU 上對于進(jìn)程(包括線程)進(jìn)行以下的活動(dòng):
(1)掛起一個(gè)進(jìn)程割去,將這個(gè)進(jìn)程在 CPU 中的狀態(tài)(上下文)存儲(chǔ)于內(nèi)存中的某處,
(2)在內(nèi)存中檢索下一個(gè)進(jìn)程的上下文并將其在 CPU 的寄存器中恢復(fù)昼丑,
(3)跳轉(zhuǎn)到程序計(jì)數(shù)器所指向的位置(即跳轉(zhuǎn)到進(jìn)程被中斷時(shí)的代碼行)呻逆,以恢復(fù)該進(jìn)程。
www.reibang.com/p/0fbeee2b2…
二菩帝、Java 內(nèi)存模型
1. Java代碼如何一步步轉(zhuǎn)化咖城,最終被CPU執(zhí)行茬腿?
最開始,我們編寫的Java代碼宜雀,是*.java文件
在編譯(javac命令)后切平,從剛才的*.java文件會(huì)變出一個(gè)新的Java字節(jié)碼文件(.class)
JVM會(huì)執(zhí)行剛才生成的字節(jié)碼文件(*.class),并把字節(jié)碼文件轉(zhuǎn)化為機(jī)器指令
機(jī)器指令可以直接在CPU上執(zhí)運(yùn)行辐董,也就是最終的程序執(zhí)行
2. 單例模式的作用和適用場景
單例的作用
節(jié)省內(nèi)存悴品、節(jié)省計(jì)算
很多情況下,我們只需要一個(gè)實(shí)例就夠了简烘,如果出現(xiàn)了更多的實(shí)例苔严,有的時(shí)候是浪費(fèi),比如:
privateResource rs =newResource();publicResourcegetExpensiveResource(){returnrs;}publicResource(){field1 =// some CPU heavy logicfield2 =// some value from DBfield3 =// etc.}復(fù)制代碼
我們花了很多時(shí)間讀取了某個(gè)文件孤澎,并保存在了內(nèi)存了届氢,那么以后就用這個(gè)實(shí)例就行了,如果每次都重新讀取覆旭,實(shí)在沒必要退子。
2.保證結(jié)果正確
有時(shí)候更多的實(shí)例更有可能是錯(cuò)誤,比如:
我們需要一個(gè)全局的計(jì)數(shù)器姐扮,來統(tǒng)計(jì)人數(shù)絮供,那么如果有多個(gè)實(shí)例,會(huì)導(dǎo)致程序結(jié)果錯(cuò)誤茶敏。
方便管理
很多工具類我們只需要一個(gè)實(shí)例壤靶,太多實(shí)例不但沒有幫助,反而會(huì)讓人眼花繚亂惊搏。
適用場景
無狀態(tài)的工具類:比如日志工具類贮乳,不管是在哪里使用,我們需要的只是它幫我們記錄日志信息恬惯,除此之外向拆,并不需要在它的實(shí)例對象上存儲(chǔ)任何狀態(tài),這時(shí)候我們就只需要一個(gè)實(shí)例對象即可酪耳。
全局信息類:比如我們在一個(gè)類上記錄網(wǎng)站的訪問次數(shù)浓恳,我們不希望有的訪問被記錄在對象A上,有的卻記錄在對象B上碗暗,這時(shí)候我們就讓這個(gè)類成為單例颈将。
3. 重排序的實(shí)例 單例模式
單例模式多種寫法、單例和高并發(fā)的關(guān)系
重要言疗,真實(shí)面試高頻考題晴圾,我遇到了不止一次了,重點(diǎn)講雙重檢查模式的volatile
餓漢式的缺點(diǎn)噪奄?
懶漢式的缺點(diǎn)死姚?
為什么要用double-check人乓?不用就不安全嗎?
為什么雙重檢查模式要用volatile都毒?
1色罚、餓漢式(靜態(tài)常量)(可用)
優(yōu)點(diǎn):這種寫法比較簡單,就是在類裝載的時(shí)候就完成實(shí)例化温鸽。避免了線程同步問題保屯。
缺點(diǎn):在類裝載的時(shí)候就完成實(shí)例化,沒有達(dá)到Lazy-Loading的效果涤垫。如果從始至終從未使用過這個(gè)實(shí)例姑尺,則會(huì)造成內(nèi)存的浪費(fèi)。
/**
* 描述:餓漢式(靜態(tài)常量)(可用)
*/publicclassSingleton{privatefinalstaticSingleton INSTANCE =newSingleton();privateSingleton(){}publicstaticSingletongetInstance(){returnINSTANCE;? ? }}復(fù)制代碼
2蝠猬、餓漢式(靜態(tài)代碼塊)(可用)
這種方式和上面的方式其實(shí)類似切蟋,只不過將類實(shí)例化的過程放在了靜態(tài)代碼塊中,也是在類裝載的時(shí)候榆芦,就執(zhí)行靜態(tài)代碼塊中的代碼柄粹,初始化類的實(shí)例。
優(yōu)缺點(diǎn)和上面是一樣的
/**
* 描述: 餓漢式(靜態(tài)代碼塊)(可用)
*/publicclassSingleton{privatefinalstaticSingleton INSTANCE;static{? ? ? ? INSTANCE =newSingleton();? ? }privateSingleton(){}publicstaticSingletongetInstance(){returnINSTANCE;? ? }}復(fù)制代碼
3匆绣、懶漢式(線程不安全)[不可用]
這種寫法起到了Lazy Loading的效果驻右,但是只能在單線程下使用。
如果在多線程下崎淳,一個(gè)線程進(jìn)入了if (singleton == null)判斷語句塊堪夭,還未來得及往下執(zhí)行
另一個(gè)線程也通過了這個(gè)判斷語句,這時(shí)便會(huì)多次創(chuàng)建實(shí)例拣凹。所以在多線程環(huán)境下不可使用這種方式森爽。
/**
* 描述:懶漢式(線程不安全)[不可用]
*/publicclassSingleton{privatestaticSingleton instance;privateSingleton(){}publicstaticSingletongetInstance(){if(instance ==null) {? ? ? ? ? ? instance =newSingleton();? ? ? ? }returninstance;? ? }}復(fù)制代碼
4、懶漢式(線程安全嚣镜,同步方法)(不推薦)
解決上面第三種實(shí)現(xiàn)方式的線程不安全問題爬迟,做個(gè)線程同步就可以了,于是就對getInstance()方法進(jìn)行了線程同步菊匿。
缺點(diǎn):效率太低了付呕,每個(gè)線程在想獲得類的實(shí)例時(shí)候,執(zhí)行g(shù)etInstance()方法都要進(jìn)行同步跌捆。而其實(shí)這個(gè)方法只執(zhí)行一次實(shí)例化代碼就夠了凡涩,后面的想獲得該類實(shí)例,直接return就行了疹蛉。方法進(jìn)行同步效率太低要改進(jìn)。
/**
* 描述:懶漢式(線程安全力麸,同步方法)(不推薦)
*/publicclassSingleton{privatestaticSingleton instance;privateSingleton(){}publicsynchronizedstaticSingletongetInstance(){if(instance ==null) {? ? ? ? ? ? instance =newSingleton();? ? ? ? }returninstance;? ? }}復(fù)制代碼
5可款、懶漢式(線程不安全育韩,同步代碼塊)[不可用]
由于第四種實(shí)現(xiàn)方式同步效率太低,所以摒棄同步方法闺鲸,改為同步產(chǎn)生實(shí)例化的的代碼塊筋讨。
但是這種同步并不能起到線程同步的作用。跟第3種實(shí)現(xiàn)方式遇到的情形一致摸恍,假如一個(gè)線程進(jìn)入了if(singleton == null)判斷語句塊悉罕,還未來得及往下執(zhí)行,另一個(gè)線程也通過了這個(gè)判斷語句立镶,這時(shí)便會(huì)產(chǎn)生多個(gè)實(shí)例壁袄。
/**
* 描述:懶漢式(線程不安全,同步代碼塊)[不可用]
*/publicclassSingleton{privatestaticSingleton instance;privateSingleton(){}publicstaticSingletongetInstance(){if(instance ==null) {synchronized(Singleton.class){? ? ? ? ? ? ? ? instance =newSingleton();? ? ? ? ? ? }? ? ? ? }returninstance;? ? }}復(fù)制代碼
6媚媒、雙重檢查(推薦面試使用)
/**
* 描述:雙重檢查(推薦面試使用)
*/publicclassSingleton{privatevolatilestaticSingleton instance;privateSingleton(){}publicstaticSingletongetInstance(){if(instance ==null) {synchronized(Singleton.class){if(instance ==null) {? ? ? ? ? ? ? ? ? ? instance =newSingleton();? ? ? ? ? ? ? ? }? ? ? ? ? ? }? ? ? ? }returninstance;? ? }}復(fù)制代碼
Double-Check概念對于多線程開發(fā)者來說不會(huì)陌生嗜逻,如代碼中所示,我們進(jìn)行了兩次if(singleton ==null)檢查缭召,這樣就可以保證線程安全了栈顷。
這樣,實(shí)例化代碼只用執(zhí)行一次嵌巷,后面再次訪問時(shí)萄凤,判斷if(singleton == null),直接return實(shí)例化對象搪哪。
這里的synchronized 不能采用synchronized(this)靡努,因?yàn)間etInstance是一個(gè)靜態(tài)方法,在它內(nèi)部不能使用未靜態(tài)的或者未實(shí)例的類對象噩死。
優(yōu)點(diǎn):線程安全颤难;延遲加載;效率較高已维。
為什么要double-check
需要第二重的原因
考慮這樣一種情況行嗤,就是有兩個(gè)線程同時(shí)到達(dá),即同時(shí)調(diào)用 getInstance() 方法垛耳,此時(shí)由于instance ==null栅屏,所以很明顯,兩個(gè)線程都可以通過第一重的 singleton== null
進(jìn)入第一重 if 語句后,由于存在鎖機(jī)制堂鲜,所以會(huì)有一個(gè)線程進(jìn)入 lock 語句并進(jìn)入第二重 singleton ==null栈雳,而另外的 一個(gè)線程則會(huì)在 lock 語句的外面等待。
而當(dāng)?shù)谝粋€(gè)線程執(zhí)行完 new Singleton()語句后缔莲,便會(huì)退出鎖定區(qū)域哥纫,此時(shí),第二個(gè)線程便可以進(jìn)入 lock 語句塊痴奏,此時(shí)蛀骇,如果沒有第二重 singleton == null 的話厌秒,那么第二個(gè)線程還是可以調(diào)用new Singleton()語句,這樣第二個(gè)線程也會(huì)創(chuàng)建一個(gè) Singleton實(shí)例擅憔,這樣也還是違背了單例模式的初衷的鸵闪,所以這里必須要使用雙重檢查鎖定。
需要第一重的原因
細(xì)心的朋友一定會(huì)發(fā)現(xiàn)暑诸,如果去掉第一重 singleton == null 蚌讼,程序還是可以在多線程下安全運(yùn)行的。
考慮在沒有第一重 singleton == null 的情況:當(dāng)有兩個(gè)線程同時(shí)到達(dá)个榕,此時(shí)篡石,由于 lock機(jī)制的存在,假設(shè)第一個(gè)線程會(huì)進(jìn)入 lock 語句塊笛洛,并且可以順利執(zhí)行 new Singleton()夏志,當(dāng)?shù)谝粋€(gè)線程退出 lock語句塊時(shí), singleton 這個(gè)靜態(tài)變量已不為 null 了苛让,所以當(dāng)?shù)诙€(gè)線程進(jìn)入 lock 時(shí)沟蔑,會(huì)被第二重singleton == null 擋在外面,而無法執(zhí)行 new Singleton()狱杰,以在沒有第一重 singleton== null 的情況下瘦材,也是可以實(shí)現(xiàn)單例模式的。
那么為什么需要第一重 singleton == null 呢仿畸?
這里就涉及一個(gè)性能問題了食棕,因?yàn)閷τ趩卫J降脑挘琻ew Singleton()只需要執(zhí)行一次就 OK 了错沽,而如果沒有第一重singleton == null的話簿晓,每一次有線程進(jìn)入getInstance()時(shí),均會(huì)執(zhí)行鎖定操作來實(shí)現(xiàn)線程同步千埃,這是非常耗費(fèi)性能的憔儿,而如果我加上第一重singleton == null的話,那么就只有在第一次執(zhí)行鎖定以實(shí)現(xiàn)線程同步放可,而以后的話谒臼,便只要直接返回 Singleton 實(shí)例就OK了,而根本無需再進(jìn)入lock語句塊了耀里,這樣就可以解決由線程同步帶來的性能問題了蜈缤。
為什么要用volatile
主要在于instance = new Singleton()這句,這并非是一個(gè)原子操作冯挎,事實(shí)上在 JVM 中這句話做了下面 3 件事情底哥。
給 instance 分配內(nèi)存
調(diào)用 Singleton 的構(gòu)造函數(shù)來初始化成員變量
將instance對象指向分配的內(nèi)存空間(執(zhí)行完這步 instance 就為非 null 了)
但是在 JVM 的即時(shí)編譯器中存在指令重排序的優(yōu)化。也就是說上面的第二步和第三步的順序是不能保證的,
最終的執(zhí)行順序可能是 1-2-3 也可能是 1-3-2叠艳。如果是后者奶陈,則在 3 執(zhí)行完畢、2 未執(zhí)行之前附较,已經(jīng)線程一被調(diào)度器暫停,此時(shí)線程二剛剛進(jìn)來第一重檢查潦俺,看到的 instance 已經(jīng)是非 null 了(但卻沒有初始化拒课,里面的值可能是null/false/0,總之不是構(gòu)造函數(shù)中指定的值)事示,所以線程二會(huì)直 接返回 instance早像,然后使用,然后順理成章地報(bào)錯(cuò)或者是看到了非預(yù)期的值(因?yàn)榇藭r(shí)屬性的值是默認(rèn)值而不是所需要的值)肖爵。
不過卢鹦,如果線程一已經(jīng)從把synchronized 同步塊的代碼都執(zhí)行完了,那么此時(shí)instance 一定是正確構(gòu)造后的實(shí)例了劝堪,這是由synchronized的heppens-before保證的冀自。
7、靜態(tài)內(nèi)部類[推薦用]
這種方式跟餓漢式方式采用的機(jī)制類似秒啦,但又有不同熬粗。
兩者都是采用了類裝載的機(jī)制來保證初始化實(shí)例時(shí)只有一個(gè)線程。
不同的地方在餓漢式方式是只要Singleton類被裝載就會(huì)實(shí)例化余境,沒有Lazy-Loading的作用驻呐,而靜態(tài)內(nèi)部類方式在Singleton類被裝載時(shí)并不會(huì)立即實(shí)例化,而是在需要實(shí)例化時(shí)芳来,調(diào)用getInstance方法含末,才會(huì)裝載SingletonInstance類,從而完成Singleton的實(shí)例化即舌。
類的靜態(tài)屬性只會(huì)在第一次加載類的時(shí)候初始化佣盒,所以在這里,JVM幫助我們保證了線程的安全性侥涵,在類進(jìn)行初始化時(shí)沼撕,別的線程是無法進(jìn)入的。
優(yōu)點(diǎn):避免了線程不安全芜飘,延遲加載务豺,效率高。
/**
* 描述: 靜態(tài)內(nèi)部類方式嗦明,可用
*/publicclassSingleton{privateSingleton(){}privatestaticclassSingletonInstance{privatestaticfinalSingleton INSTANCE =newSingleton();? ? }publicstaticSingletongetInstance(){returnSingletonInstance.INSTANCE;? ? }}復(fù)制代碼
8笼沥、枚舉[推薦用]
借助JDK1.5中添加的枚舉來實(shí)現(xiàn)單例模式。不僅能避免多線程同步問題,還是懶加載奔浅,而且還能防止 反序列化重新創(chuàng)建新的對象馆纳。
/**
* 描述:枚舉單例
*/publicenumSingleton {? ? INSTANCE;publicvoidwhatever(){? ? }}復(fù)制代碼
4. 單例各種寫法的適用場合
最好的方法是利用枚舉,因?yàn)檫€可以防止反序列化重新創(chuàng)建新的對象汹桦;
非線程同步的方法不能使用鲁驶;
如果程序一開始要加載的資源太多,那么就應(yīng)該使用懶加載舞骆;
餓漢式如果是對象的創(chuàng)建需要配置文件就不適用钥弯。
懶加載雖然好,但是靜態(tài)內(nèi)部類這種方式會(huì)引入編程復(fù)雜性督禽,大部分情況還是推薦枚舉脆霎,簡單。
5. 應(yīng)該如何選擇狈惫,用哪種單例的實(shí)現(xiàn)方案最好睛蛛?
結(jié)論
枚舉最好
具體解釋
Joshua Bloch大神在《Effective Java》中明確表達(dá)過的觀點(diǎn):“使用枚舉實(shí)現(xiàn)單例的方法雖然還沒有廣泛采用,但是單元素的枚舉類型已經(jīng)成為實(shí)現(xiàn)Singleton的最佳方法胧谈∫渖觯”
寫法簡單
枚舉比任何其他的寫法都簡單,很優(yōu)雅第岖。
publicenumSingleton {? ? INSTANCE;publicvoidwhatever(){? ? }}復(fù)制代碼
線程安全有保障
我們定義的一個(gè)枚舉难菌,在第一次被真正用到的時(shí)候,會(huì)被虛擬機(jī)加載并初始化蔑滓,而這個(gè)初始化過程是線程安全的郊酒。
而我們知道,解決單例的并發(fā)問題键袱,主要解決的就是初始化過程中的線程安全問題燎窘。
所以,由于枚舉的以上特性蹄咖,枚舉實(shí)現(xiàn)的單例是天生線程安全的褐健。
避免反序列化破壞單例
普通的Java類的反序列化過程中,會(huì)通過反射調(diào)用類的默認(rèn)構(gòu)造函數(shù)來初始化對象澜汤。
所以蚜迅,即使單例中構(gòu)造函數(shù)是私有的,也會(huì)被反射給破壞掉俊抵。
由于反序列化后的對象是重新new出來的谁不,所以這就破壞了單例。
但是徽诲,枚舉的反序列化并不是通過反射實(shí)現(xiàn)的刹帕。所以吵血,也就不會(huì)發(fā)生由于反序列化導(dǎo)致的單例破壞問題。
6. 講一講什么是Java內(nèi)存模型?
這個(gè)問題很大偷溺,給了咱們足夠的發(fā)揮空間蹋辅。
我們按照這個(gè)思路去回答:
從上到下,按照思維導(dǎo)圖的內(nèi)容一點(diǎn)一點(diǎn)地說挫掏,內(nèi)容比較豐富:
先講三兄弟:JVM內(nèi)存結(jié)構(gòu) VS Java內(nèi)存模型 VS Java對象模型
然后介紹JMM的作用和地位侦另,最重要的3個(gè)部分是重排序、可見性砍濒、原子性淋肾。
然后展開介紹,分別結(jié)束重排序的好處爸邢、情況;
可見性的問題描述和volatile,原子性的操作等拿愧。
剩下的詳見思維導(dǎo)圖杠河,咱們挑選咱們擅長的講,最好能當(dāng)場在白紙上畫思維導(dǎo)圖出來浇辜,絕對是加分項(xiàng)券敌。
7. 什么是happens-before?
什么是 happens-before
happens-before規(guī)則是用來解決可見性問題的:在時(shí)間上柳洋,動(dòng)作A發(fā)生在動(dòng)作B之前待诅,B保證能看見A,這就是happens-before熊镣。
兩個(gè)操作可以用happens-before來確定它們的執(zhí)行順序:如果一個(gè)操作happens-before于另一個(gè)操作,那么我們說第一個(gè)操作對于第二個(gè)操作是可見的卑雁。
什么不是happens-before
這樣說不太直觀,我們直接來看绪囱,什么不是happens-before:
由于兩個(gè)線程沒有相互配合的機(jī)制测蹲,所以代碼x和Y的可見性無法保證,比如X修改了a鬼吵,但是Y可能看得見也可能看不見扣甲,那么就不是happens-before。
影響JVM重排序
如果兩個(gè)操作不具備happens-before齿椅,那么JVM是可以根據(jù)需要自由排序的
但是如果具備happens-before (比如新建線程時(shí)琉挖,例如thread.start() happens before run方法里面的語句),那么JVM也不能改變它們之間的順序讓happens-before失效涣脚。
8. Happens-Before規(guī)則有哪些?
如果我們分別有操作x和操作y示辈,我們用hb(x,y)來表示 x happens-before y
單線程規(guī)則
單個(gè)線程中的每個(gè)操作都happens-before該程序順序中稍后出現(xiàn)的該線程中的每個(gè)操作。
如果操作x和操作y是同一個(gè)線程的兩個(gè)操作涩澡,并且在代碼執(zhí)行上x先于y出現(xiàn)顽耳,那么有 hb(x,y)
注意:x happens-before y坠敷,并不是說x操作一定要在y操作之前被執(zhí)行,而是說x的執(zhí)行結(jié)果對于y是可見的射富,只要滿足可見性,發(fā)生了重排序也是可以的膝迎。
鎖操作(synchronized 和 Lock)
如果A是對鎖的解鎖un1ock,而B是對同一個(gè)鎖的加鎖lock胰耗,那么hb(A,B)
volatile變量
假設(shè)A是對volati1e變量的寫操作限次,B是對這個(gè)變量的讀操作,那么hb(A,B)柴灯。
這就保證了卖漫,用volati1e修飾的變量,每次被修改后赠群,所有線程都能看得到羊始。
線程啟動(dòng)
新建線程時(shí),thread.start()一定happens-before run方法里面的語句查描。
線程join
我們知道join可以讓線程之間等待突委,假設(shè)線程A通過調(diào)用threadA.start ()生成一個(gè)新線程B,然后調(diào)用threadA.join()冬三。
線程A將在join ()期間會(huì)等待匀油,直到線程B的run方法完成。
在join方法返回后勾笆,線程A中的所有后續(xù)操作都將看到線程B的run方法中執(zhí)行的所有操作敌蚜,也就是happens-before。
(線程的終止也類似窝爪,isAlive返回false一定在線程所有代碼執(zhí)行完之前)
傳遞性
如果hb(A, B) 而且hb(B, C)弛车,那么可以推出hb(A, C)
中斷
一個(gè)線程被其他線程interrupt,那么檢測中斷(isInterrupted)或者拋出InterruptedException時(shí)一定能看到酸舍。
構(gòu)造方法
對象構(gòu)造方法的最后一行指令happens-before于 finalize()方法的第一行指令帅韧。
工具類的Happens-Before原則
線程安全的容器get—定能看到在此之前的put等存入動(dòng)作
CountDownLatch
Semaphore
Future
線程池
Cyc1icBarrier
9. 講講volatile關(guān)鍵字?
volatile是什么
volatile是一種同步機(jī)制,比synchronized或者Lock相關(guān)類更輕量啃勉,因?yàn)槭褂胿olatile并不會(huì)發(fā)生上下文切換等開銷很大的行為忽舟。
如果一個(gè)變量別修飾成volatile,那么JVM就知道了這個(gè)變量可能會(huì)被并發(fā)修改淮阐。
但是開銷小叮阅,相應(yīng)的能力也小,雖然說volatile是用來同步的保證線程安全的泣特,但是volatile做不到synchronized那樣的原子保護(hù)浩姥,volatile僅在很有限的場景下才能發(fā)揮作用。
volatile的適用場合
不適用:a++
volatile不適用于構(gòu)建原子操作状您,比如更新時(shí)需要依據(jù)原來的值的時(shí)候勒叠,最典型的是a++兜挨,而這是不能靠volatile來保證原子性的。
適用場合1:boolean flag
某個(gè)屬性被多個(gè)線程共享眯分,其中有一個(gè)線程修改了此volatile屬性拌汇,其他線程可以立即得 到修改后的值。在并發(fā)包的源碼中弊决,它使用得非常多噪舀。
如果一個(gè)共享變量自始至終只被各個(gè)線程賦值,而沒有其他的操作飘诗,那么就可以用volatile來代替synchronized或者代替原子變量与倡,因?yàn)橘x值自身是有原子性的,而volatile又保證了可見性昆稿,所以就足以保證線程安全
比如最典型的是一個(gè)boolean的flag纺座,只要flag變了,所有線程都能立刻看到溉潭,這里不存在 復(fù)合操作(例如a++)比驻,這里只存在單一操作就是改變flag的值,所以很適用于volatile岛抄。
適用場合2:作為刷新之前變量的觸發(fā)器
用了volatile int x后,可以保證讀取x后狈茉,之前的所有變量都可見夫椭。一個(gè)實(shí)際生產(chǎn)中的例子:
Map configoptions;char[] configText;volatilebooleaninitialized =false;// Thread Aconfigoptions =newHashMap();configText = readConfigFile(fileName);processConfigoptions( configText, configoptions) ;initialized =true;// Thread Bwhile(!initialized)sleep();// use configoptions復(fù)制代碼
volatile的作用:
第一層:可見性
可見性指的是在一個(gè)線程中對該變量的修改會(huì)馬上由工作內(nèi)存(Work Memory)寫回主內(nèi)存(Main Memory),所以會(huì)馬上反應(yīng)在其它線程的讀取操作中氯庆。
工作內(nèi)存是線程獨(dú)享的蹭秋,主存是線程共享的。
我們還是用 JMM 的主內(nèi)存和本地內(nèi)存抽象來描述样悟,這樣比較準(zhǔn)確晚凿。還有蝶俱,并不是只有Java 語言才有volatile 關(guān)鍵字,所以后面的描述一定要建立在 Java 跨平臺(tái)以后抽象出了內(nèi)存模型的這個(gè)大環(huán)境下洞豁。
還記得 synchronized 的語義嗎?進(jìn)入 synchronized 時(shí)荒给,使得本地緩存失效丈挟,synchronized 塊中對共享變量的讀取必須從主內(nèi)存讀取志电;退出 synchronized 時(shí)曙咽,會(huì)將進(jìn)入 synchronized 塊之前synchronized塊中的寫操作刷入到主存中。
volatile 有類似的語義挑辆,讀一個(gè) volatile 變量之前例朱,需要先使相應(yīng)的本地緩存失效孝情,這樣就必須到主內(nèi)存讀取最新值,寫一個(gè) volatile 屬性會(huì)立即刷入到主內(nèi)存洒嗤。
所以箫荡,volatile 讀和 monitorenter 有相同的語義,volatile 寫和 monitorexit 有相同的語義烁竭。
第二層:禁止指令重排序優(yōu)化
volatile的第二層意思是禁止指令重排序優(yōu)化菲茬。
由于編譯器優(yōu)化,在實(shí)際執(zhí)行的時(shí)候可能與我們編寫的順序不同派撕。編譯器只保證程序執(zhí)行結(jié)果與源代碼相同婉弹,卻不保證實(shí)際指令的順序與源代碼相同。這 在單線程看起來沒什么問題终吼,然而一旦引入多線程镀赌,這種亂序就可能導(dǎo)致嚴(yán)重問題。
volatile關(guān)鍵字就可以解決這個(gè)問題际跪。大家還記得的雙重檢查的單例模式吧商佛,前面提到,加個(gè) volatile 能解決問題姆打。其實(shí)就是利用了 volatile的禁止重排序功能良姆。
volatile 的禁止重排序并不局限于兩個(gè) volatile 的屬性操作不能重排序,而且是 volatile屬性操作和它周圍的普通屬性的操作也不能重排序幔戏。
之前 instance = new Singleton() 中玛追,如果 instance 是 volatile 的,那么對于 instance 的賦值操作(賦一個(gè)引用給 instance 變量)就不會(huì)和構(gòu)造函數(shù)中的屬性賦值發(fā)生重排序闲延,能保證構(gòu)造方法結(jié)束后痊剖,才將此對象引用賦值給 instance。
根據(jù) volatile 的內(nèi)存可見性和禁止重排序垒玲,那么我們不難得出一個(gè)推論:線程 a 如果寫入一個(gè) volatile變量陆馁,此時(shí)線程 b 再讀取這個(gè)變量,那么此時(shí)對于線程 a 可見的所有屬性對于線程 b 都是可見的合愈。
volatile小結(jié)
volatile 修飾符適用于以下兩種場景:
第一種是某個(gè)屬性被多個(gè)線程共享叮贩,其中有一個(gè)線程修改了此屬性,其他線程可以立即得到修改后的值想暗,比如boolean flag妇汗。
第二種是作為觸發(fā)器,實(shí)現(xiàn)輕量級同步说莫。
volatile 屬性的讀寫操作都是無鎖的杨箭,它不能替代 synchronized,因?yàn)樗鼪]有提供原子性和互斥性储狭。因?yàn)闊o鎖互婿,不需要花費(fèi)時(shí)間在獲取鎖和釋放鎖上捣郊,所以說它是低成本的。
volatile 只能作用于屬性慈参,我們用 volatile 修飾屬性呛牲,這樣 compilers 就不會(huì)對這個(gè)屬性做指令重排序。
volatile 提供了可見性驮配,任何一個(gè)線程對其的修改將立馬對其他線程可見娘扩。volatile 屬性不會(huì)被線程緩存,始終從主存中讀取壮锻。
volatile 提供了 happens-before 保證琐旁,對 volatile 變量 v 的寫入 happens-before 所有其他線程后續(xù)對 v 的讀操作。
volatile 可以使得 long 和 double 的賦值是原子的猜绣。
10. volatile和synchronized的異同灰殴?
volatile可以看做是輕量版的synchronized:如果一個(gè)共享變量自始至終只被各個(gè)線程賦值,而沒有其他的操作掰邢,那么就可以用volatile來代替synchronized或者代替原子變量牺陶,因?yàn)橘x值自身是有原子性的,而volatile又保證了可見性辣之,所以就足以保證線程安全掰伸。
11. 什么是內(nèi)存可見性問題?
CPU有多級緩存怀估,導(dǎo)致讀的數(shù)據(jù)過期 為了提高CPU的運(yùn)行效率碱工,CPU內(nèi)加入了高速緩存,高速緩存的容量比主內(nèi)存小奏夫,但是速度僅次于寄存器,所以在CPU和主內(nèi)存之間就多了Cache層历筝,導(dǎo)致了多線程時(shí)很多問題的發(fā)生酗昼。
線程間的對于共享變量的可見性問題不是直接由多核引起的,而是由多緩存引起的梳猪。如果所有個(gè)核心都只用一個(gè)緩存麻削,那么也就不存在內(nèi)存可見性問題了。
現(xiàn)代多核 CPU 中每個(gè)核心擁有自己的一級緩存或一級緩存加上二級緩存等春弥,問題就發(fā)生在每個(gè)核心的獨(dú)占緩存上呛哟。每個(gè)核心都會(huì)將自己需要的數(shù)據(jù)讀到獨(dú)占緩存中,數(shù)據(jù)修改后也是寫入到緩存中匿沛,然后等待刷入到主存中扫责。所以會(huì)導(dǎo)致有些核心讀取的值是一個(gè)過期的值。
CPU緩存結(jié)構(gòu)圖
Java 作為高級語言逃呼,屏蔽了這些底層細(xì)節(jié)鳖孤,用 JMM 定義了一套讀寫內(nèi)存數(shù)據(jù)的規(guī)范者娱,雖然我們不再需要關(guān)心一級緩存和二級緩存的問題,但是苏揣,JMM 抽象了主內(nèi)存和本地內(nèi)存的概念黄鳍。
這里說的本地內(nèi)存并不是真的是一塊給每個(gè)線程分配的內(nèi)存,而是 JMM 的一個(gè)抽象平匈,是對于寄存器框沟、一級緩存、二級緩存等的抽象增炭。
12. 主內(nèi)存和本地內(nèi)存的關(guān)系
JMM有以下規(guī)定:
所有的變量都存儲(chǔ)在主內(nèi)存中忍燥,同時(shí)每個(gè)線程也有自己獨(dú)立的工作內(nèi)存,工作內(nèi)存中的變量內(nèi)容是主內(nèi)存中的拷貝
線程不能直接讀寫主內(nèi)存中的變量,而是只能操作自己工作內(nèi)存中的變量弟跑,然后再同步到主內(nèi)存中
主內(nèi)存是多個(gè)線程共享的灾前,但線程間不共享工作內(nèi)存,如果線程間需要通信,必須借助主內(nèi)存中轉(zhuǎn)來完成所有的共享變量存在于主內(nèi)存中孟辑,每個(gè)線程有自己的本地內(nèi)存哎甲,而且線程讀寫共享數(shù)據(jù)也是通過本地內(nèi)存交換的,所以才導(dǎo)致了可見性問題饲嗽。操作系統(tǒng)會(huì)盡可能抓住機(jī)會(huì)同步(只要CPU等有空閑炭玫,就去同步),而不是等要用的時(shí)候再同步貌虾。
13. 什么是原子操作?
原子操作是指一系列的操作吞加,要么全部執(zhí)行成功,要么全部不執(zhí)行尽狠,不會(huì)出現(xiàn)執(zhí)行一半的情況衔憨,是不可分割的。
例如從ATM里取錢袄膏,是一個(gè)原子操作践图,而其中包含扣余額、吐出現(xiàn)金沉馆、銀行系統(tǒng)記錄等一系列操作码党,雖然不只是一個(gè)操作,但是由于這一些列操作要么全部執(zhí)行成功斥黑,要么全部不執(zhí)行揖盘,不會(huì)出現(xiàn)執(zhí)行一半的情況,所以從ATM里取錢就是具有原子性的。
例如i++這一行代碼到CPU里锌奴,是3個(gè)指令:
讀取i的舊值兽狭;
計(jì)算i的新值;
把新值寫回去。
這說明i++不是原子性的椭符。
但是如果我們把i++這行代碼加了synchronized鎖荔燎,那么它就變成原子的了,因?yàn)闆]有其他線程可以在它修改i時(shí)同時(shí)操作i销钝。
14. Java中的原子操作有哪些?
除long和doub1e之外的基本類型(int有咨,byte, boolean, short, char, float)的賦值操作
所有引用reference的賦值操作,不管是32位的機(jī)器還是64位的機(jī)器
java.concurrent.Atomic.*包中所有類的原子操作
15. long 和 double 的原子性你了解嗎蒸健?
問題描述
long 和 double座享,它們的值需要占用 64 位的內(nèi)存空間,Java 編程語言規(guī)范中提到似忧,對于64 位的值的寫 入渣叛,可以分為兩個(gè) 32 位的操作進(jìn)行寫入。
本來一個(gè)整體的賦值操作盯捌,被拆分為低 32 位賦值和高 32位賦值兩個(gè)操作淳衙,中間如果發(fā)生了其他線程對于這個(gè)值的讀操作,必然就會(huì)讀到一個(gè)奇怪的值饺著。
看官方文檔
docs.oracle.com/javase/spec…
這個(gè)時(shí)候我們要使用 volatile 關(guān)鍵字進(jìn)行控制了箫攀,JMM 規(guī)定了對于 volatile long 和 volatile double,JVM 需要保證寫入操作的原子性幼衰。
結(jié)論
在32位上的JVM上靴跛,long 和 double的操作不是原子的,但是在64位的JVM上是原子的渡嚣。
16. 生成對象的過程是不是原子操作梢睛?
生成對象的過程不是原子操作,實(shí)際上是3個(gè)步驟的組合
已Person p = new Person()為例
新建一個(gè)空的Person對象
把這個(gè)對象的地址指向p
執(zhí)行Person的構(gòu)造函數(shù)
這3個(gè)動(dòng)作的執(zhí)行順序是可能被改變的识椰。
三绝葡、死鎖
1. 寫一個(gè)必然死鎖的例子?
分析
當(dāng)類的對象flag=1時(shí)(T1)腹鹉,先鎖定O1,睡眠500毫秒挤牛,然后鎖定O2;
而T1在睡眠的時(shí)候另一個(gè)flag=0的對象(T2)線程啟動(dòng)种蘸,先鎖定O2,睡眠500毫秒,等待T1釋放O1竞膳;
T1睡眠結(jié)束后需要鎖定O2才能繼續(xù)執(zhí)行航瞭,而此時(shí)O2已被T2鎖定;
T2睡眠結(jié)束后需要鎖定O1才能繼續(xù)執(zhí)行坦辟,而此時(shí)O1已被T1鎖定刊侯;
T1、T2相互等待锉走,都需要對方鎖定的資源才能繼續(xù)執(zhí)行滨彻,從而死鎖藕届。
更嚴(yán)謹(jǐn)?shù)脑挘梢钥紤]用CountDownLatch亭饵,因?yàn)閟tart不代表就是運(yùn)行休偶。
/**
* 描述:必定發(fā)生死鎖的情況
*/publicclassMustDeadLockimplementsRunnable{intflag =1;staticObject o1 =newObject();staticObject o2 =newObject();publicstaticvoidmain(String[] args){? ? ? ? MustDeadLock r1 =newMustDeadLock();? ? ? ? MustDeadLock r2 =newMustDeadLock();? ? ? ? r1.flag =1;? ? ? ? r2.flag =0;? ? ? ? Thread t1 =newThread(r1);? ? ? ? Thread t2 =newThread(r2);? ? ? ? t1.start();? ? ? ? t2.start();? ? }@Overridepublicvoidrun(){? ? ? ? System.out.println("flag = "+ flag);if(flag ==1) {synchronized(o1) {try{? ? ? ? ? ? ? ? ? ? Thread.sleep(500);? ? ? ? ? ? ? ? }catch(InterruptedException e) {? ? ? ? ? ? ? ? ? ? e.printStackTrace();? ? ? ? ? ? ? ? }synchronized(o2) {? ? ? ? ? ? ? ? ? ? System.out.println("線程1成功拿到兩把鎖");? ? ? ? ? ? ? ? }? ? ? ? ? ? }? ? ? ? }if(flag ==0) {synchronized(o2) {try{? ? ? ? ? ? ? ? ? ? Thread.sleep(500);? ? ? ? ? ? ? ? }catch(InterruptedException e) {? ? ? ? ? ? ? ? ? ? e.printStackTrace();? ? ? ? ? ? ? ? }synchronized(o1) {? ? ? ? ? ? ? ? ? ? System.out.println("線程2成功拿到兩把鎖");? ? ? ? ? ? ? ? }? ? ? ? ? ? }? ? ? ? }? ? }}復(fù)制代碼
2. 生產(chǎn)中什么場景下會(huì)發(fā)生死鎖?
最明顯的情況是在一個(gè)方法內(nèi)需要獲取多個(gè)鎖辜羊。
但是也有不明顯的情況踏兜,比如在一個(gè)方法里先獲取一把鎖,然后調(diào)用其他方法八秃,而其他方法是需要獲取鎖的碱妆,這樣實(shí)際上也形成了鎖的鏈路,也會(huì)造成死鎖昔驱。
3. 發(fā)生死鎖必須滿足哪些條件疹尾?
死鎖必須同時(shí)滿足以下4個(gè)條件,才可能發(fā)生并且持續(xù)發(fā)生骤肛,缺一不可:
互斥條件
一個(gè)資源每次只能被一個(gè)進(jìn)程使用纳本。
請求與保持條件
一個(gè)進(jìn)程因請求資源而阻塞時(shí),對已獲得的資源保持不放萌衬。
不剝奪條件
進(jìn)程已獲得的資源饮醇,在末使用完之前,不能強(qiáng)行剝奪秕豫。
循環(huán)等待條件
若干進(jìn)程之間形成一種頭尾相接的循環(huán)等待資源關(guān)系朴艰。
逐個(gè)分析
把剛才寫的必然死鎖的例子分析一下,看剛才的必然死鎖的例子中混移,是否一一滿足了這4個(gè)條件祠墅。
4. 如何用工具定位死鎖?
方法1:jstack
用jstack或者jconsole獲取線程棧歌径,如果是比較明顯的死鎖關(guān)系毁嗦,工具可以直接檢測出來;如果死鎖不明顯回铛,軟件無法直接檢測出來狗准,那么我們就可以分析線程棧,然后就可以發(fā)現(xiàn)相互的依賴關(guān)系茵肃。
這里面包含了線程加鎖信息腔长,例如哪些線程獲取了哪個(gè)鎖,以及在哪個(gè)語句中獲取的验残,以及正在等待或者阻塞的線程是哪個(gè)等重要信息捞附。
先運(yùn)行MustDeadLock類
用Sloth或者命令行查看到j(luò)ava的pid,然后執(zhí)行${JAVA_HOME}/bin/jstack pid
這樣容易排查,可以看出,Thread-1持有尾號688的鎖鸟召,需要678的鎖胆绊,而Thread-0恰恰相反,Thread-0持有678的鎖欧募,需要688的鎖压状,死鎖發(fā)生!
用jstack分析各線程持有的鎖和需要的鎖槽片,然后分析是否有相互循環(huán)請求的死鎖的情況何缓。
然后就會(huì)提示:
方法2:ThreadMXBean
ThreadMXBean類的使用:
publicclassThreadMXBeanDetectionimplementsRunnable{publicstaticvoidmain(String[] args)throwsInterruptedException{? ? ? ? ? ? ThreadMXBeanDetection r1 =newThreadMXBeanDetection();? ? ? ? ? ? ThreadMXBeanDetection r2 =newThreadMXBeanDetection();? ? ? ? ? ? r1.flag =1;? ? ? ? ? ? r2.flag =0;? ? ? ? ? ? Thread t1 =newThread(r1);? ? ? ? ? ? Thread t2 =newThread(r2);? ? ? ? ? ? t1.start();? ? ? ? ? ? t2.start();? ? ? ? ? ? Thread.sleep(1000);? ? ? ? ? ? ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();long[] deadlockedThreads = threadMXBean.findDeadlockedThreads();if(deadlockedThreads !=null&& deadlockedThreads.length >0) {for(inti =0; i < deadlockedThreads.length; i++) {? ? ? ? ? ? ? ? ? ? ThreadInfo threadInfo = threadMXBean.getThreadInfo(deadlockedThreads[i]);? ? ? ? ? ? ? ? ? ? System.out.println("發(fā)現(xiàn)死鎖"+ threadInfo.getThreadName());? ? ? ? ? ? ? ? }? ? ? ? ? ? }? ? ? ? }}復(fù)制代碼
5. 有哪些解決死鎖問題的策略?
常見修復(fù)策略
1.避免策略(哲學(xué)家就餐的換手方案还栓、轉(zhuǎn)賬換序方案碌廓、不講銀行家算法)
2.檢測與恢復(fù)策略(一段時(shí)間檢測是否有死鎖,如果有就剝奪某一個(gè)資源剩盒,來打開死鎖)
3.鴕鳥策略:鴕鳥這種動(dòng)物在遇到危險(xiǎn)的時(shí)候谷婆,通常就會(huì)把頭埋在地上,這樣一來它就看不到危險(xiǎn)了辽聊。而鴕鳥策略的意思就是說纪挎,如果我們發(fā)生死鎖的概率極其低,那么我們就直接忽略它跟匆,直到死鎖發(fā)生的時(shí)候异袄,再人工修復(fù)。
避免策略
如何避免
如果我們能避免相反的獲取鎖的順序玛臂,就可以避免死鎖的發(fā)生烤蜕,所以我們從這個(gè)角度出發(fā),去避免死鎖迹冤。
實(shí)際上不在乎獲取鎖的順序
其實(shí)對于我們而言讽营,在轉(zhuǎn)賬時(shí)只要能獲取到兩把鎖就可以,而不在乎這兩把鎖的獲取順序泡徙,無論是先獲取哪把鎖或者后獲取哪把鎖橱鹏,只要最終獲取到了兩把鎖,就可以開始轉(zhuǎn)賬堪藐。我們使用HashCode的值來決定順序莉兰,從而保證了線程安全。
通過hashcode來決定獲取鎖的順序礁竞,沖突時(shí)需要“加時(shí)賽”
修正的關(guān)鍵在于糖荒,多個(gè)線程對兩把鎖的獲取順序都保持一致,這樣就不會(huì)有相互等待的情況發(fā)生:我們用hashcode來作為對賬戶Account的標(biāo)識id苏章,并始終先從hash值小的賬戶開始獲取鎖。
加了Thread.sleep(500)也不會(huì)發(fā)生死鎖,只不過是第二次轉(zhuǎn)賬會(huì)等第一次轉(zhuǎn)賬結(jié)束后再進(jìn)行而已枫绅。
但是考慮一個(gè)特殊情況:Hash沖突泉孩。一旦兩個(gè)對象的Hash值一樣,就需要“加時(shí)賽”來決定鎖的獲取順序并淋,這樣就可以萬無一失地避免死鎖:
有主鍵就更方便
我們剛才選用hashcode作為排序的標(biāo)準(zhǔn)寓搬,是因?yàn)閔ashcode比較通用,每個(gè)對象都有县耽。不過在實(shí)際生產(chǎn)中句喷,需要排序的往往是一個(gè)實(shí)體類,而每一個(gè)實(shí)體類一般都會(huì)有主鍵ID兔毙,主鍵ID具有唯一不重復(fù)的特點(diǎn)唾琼,所以如果我們的類包含主鍵,那么就方便多了澎剥,直接根據(jù)主鍵的順序來決定鎖的獲取順序锡溯,也不再需要“加時(shí)賽”了,就可以避免死鎖哑姚。
intfromHash = System.identityHashCode(from);inttoHash = System.identityHashCode(to);if(fromHash < toHash) {synchronized(from) {synchronized(to) {newHelper().transfer();? ? ? ? ? ? ? ? }? ? ? ? ? ? }? ? ? ? }elseif(fromHash > toHash) {synchronized(to) {synchronized(from) {newHelper().transfer();? ? ? ? ? ? ? ? }? ? ? ? ? ? }? ? ? ? }else{synchronized(lock) {synchronized(to) {synchronized(from) {newHelper().transfer();? ? ? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? }? ? ? ? ? ? }? ? ? ? }復(fù)制代碼
完整代碼:
/**
* 描述: 轉(zhuǎn)賬時(shí)候遇到死鎖
*/publicclassTransferMoneyimplementsRunnable{intflag =1;staticAccount a =newAccount(500);staticAccount b =newAccount(500);staticObjectlock=newObject();publicstaticvoidmain(String[] args) throws InterruptedException{? ? ? ? TransferMoney r1 =newTransferMoney();? ? ? ? TransferMoney r2 =newTransferMoney();? ? ? ? r1.flag =1;? ? ? ? r2.flag =0;? ? ? ? Thread t1 =newThread(r1);? ? ? ? Thread t2 =newThread(r2);? ? ? ? t1.start();? ? ? ? t2.start();? ? ? ? t1.join();? ? ? ? t2.join();? ? ? ? System.out.println("a的余額"+ a.balance);? ? ? ? System.out.println("b的余額"+ b.balance);? ? }? ? @Overridepublicvoidrun(){if(flag ==1) {? ? ? ? ? ? transferMoney(a, b,200);? ? ? ? }if(flag ==0) {? ? ? ? ? ? transferMoney(b, a,200);? ? ? ? }? ? }publicstaticvoidtransferMoney(Accountfrom, Account to,intamount){classHelper{publicvoidtransfer(){if(from.balance - amount <0) {? ? ? ? ? ? ? ? ? ? System.out.println("余額不足祭饭,轉(zhuǎn)賬失敗。");return;? ? ? ? ? ? ? ? }from.balance -= amount;? ? ? ? ? ? ? ? to.balance = to.balance + amount;? ? ? ? ? ? ? ? System.out.println("成功轉(zhuǎn)賬"+ amount +"元");? ? ? ? ? ? }? ? ? ? }intfromHash = System.identityHashCode(from);inttoHash = System.identityHashCode(to);if(fromHash < toHash) {? ? ? ? ? ? synchronized (from) {? ? ? ? ? ? ? ? synchronized (to) {newHelper().transfer();? ? ? ? ? ? ? ? }? ? ? ? ? ? }? ? ? ? }elseif(fromHash > toHash) {? ? ? ? ? ? synchronized (to) {? ? ? ? ? ? ? ? synchronized (from) {newHelper().transfer();? ? ? ? ? ? ? ? }? ? ? ? ? ? }? ? ? ? }else{? ? ? ? ? ? synchronized (lock) {? ? ? ? ? ? ? ? synchronized (to) {? ? ? ? ? ? ? ? ? ? synchronized (from) {newHelper().transfer();? ? ? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? }? ? ? ? ? ? }? ? ? ? }? ? }staticclassAccount{publicAccount(intbalance){this.balance = balance;? ? ? ? }intbalance;? ? }}復(fù)制代碼
檢測與恢復(fù)策略
什么是死鎖檢測算法
和之前的死鎖避免的思路不同叙量,這里的死鎖檢測倡蝙,是首先允許系統(tǒng)發(fā)生死鎖,但是每次調(diào)用鎖的時(shí)候绞佩,都記錄在案寺鸥,維護(hù)一個(gè)“鎖的調(diào)用鏈路圖”。
隔一段時(shí)間就來用死鎖檢測算法檢查一下征炼,搜索“鎖的調(diào)用鏈路圖”中是否存在環(huán)路析既,一旦發(fā)生死鎖,就用死鎖恢復(fù)機(jī)制進(jìn)行恢復(fù)谆奥。
死鎖恢復(fù):
方法1——進(jìn)程終止
逐個(gè)終止線程眼坏,直到死鎖消除。
順序
優(yōu)先級(是前臺(tái)交互還是后臺(tái)處理)
已占用資源酸些、還需要的資源
已經(jīng)運(yùn)行時(shí)間 等…
方法2——資源搶占
通過把已經(jīng)分發(fā)出去的鎖給收回來的資源搶占方式宰译,讓線程回退幾步,這樣就不用結(jié)束整個(gè)線程魄懂,成本比較低沿侈。
缺點(diǎn):可能同一個(gè)線程一直被搶占,那就造成饑餓市栗。
6. 講一講經(jīng)典的哲學(xué)家就餐問題缀拭?
偽代碼
while(true) {// Initially, thinking about life, universe, and everythingthink();// Take a break from thinking, hungry nowpick_up_left_fork();pick_up_right_fork();eat();put_down_right_fork();put_down_left_fork();// Not hungry anymore. Back to thinking!}復(fù)制代碼
有死鎖和資源耗盡的風(fēng)險(xiǎn)
哲學(xué)家從來不交談咳短,這就很危險(xiǎn),可能產(chǎn)生死鎖蛛淋,每個(gè)哲學(xué)家都拿著左手的餐叉咙好,永遠(yuǎn)都在等右邊的餐叉(或者相反)。即使沒有死鎖褐荷,也有可能發(fā)生資源耗盡勾效。例如,假設(shè)規(guī)定當(dāng)哲學(xué)家等待另一只餐叉超過五分鐘后就放下自己手里的那一只餐叉叛甫,并且再等五分鐘后進(jìn)行下一次嘗試层宫。這個(gè)策略消除了死鎖(系統(tǒng)總會(huì)進(jìn)入到下一個(gè)狀態(tài)),但仍然有可能發(fā)生“活鎖”其监。如果五位哲學(xué)家在完全相同的時(shí)刻進(jìn)入餐廳萌腿,并同時(shí)拿起左邊的餐叉,那么這些哲學(xué)家就會(huì)等待五分鐘同時(shí)放下手中的餐叉棠赛,再等五分鐘哮奇,又同時(shí)拿起這些餐叉。
在實(shí)際的計(jì)算機(jī)問題中睛约,缺乏餐叉可以類比為缺乏共享資源鼎俘。一種常用的計(jì)算機(jī)技術(shù)是資源加鎖,用來保證在某個(gè)時(shí)刻辩涝,資源只能被一個(gè)程序或一段代碼訪問贸伐。當(dāng)一個(gè)程序想要使用的資源已經(jīng)被另一個(gè)程序鎖定,它就等待資源解鎖怔揩。當(dāng)多個(gè)程序涉及到加鎖的資源時(shí)捉邢,在某些情況下就有可能發(fā)生死鎖。例如商膊,某個(gè)程序需要訪問兩個(gè)文件伏伐,當(dāng)兩個(gè)這樣的程序各鎖了一個(gè)文件,那它們都在等待對方解鎖另一個(gè)文件晕拆,而這永遠(yuǎn)不會(huì)發(fā)生藐翎。
多種解決方案
服務(wù)員檢查
我們可以要求引入一個(gè)服務(wù)員來解決,每當(dāng)哲學(xué)家需要吃飯的時(shí)候实幕,他就詢問服務(wù)員我現(xiàn)在是否能吃飯吝镣,這樣服務(wù)員就會(huì)去檢查,如果死鎖將要發(fā)生昆庇,服務(wù)員就不允許哲學(xué)家吃飯末贾。
改變一個(gè)哲學(xué)家拿叉子的順序
我們可以讓四個(gè)哲學(xué)家先拿左邊的叉子,再拿右邊的叉子整吆,而其中一名哲學(xué)家和他們相反拱撵,先拿右邊的叉子辉川,再拿左邊的叉子,這樣一來就不會(huì)出現(xiàn)循環(huán)等待同一邊叉子的情況拴测。
餐票
哲學(xué)家吃飯前员串,必須先看看有沒有剩余的餐票,而餐票只有4張昼扛,所以5個(gè)哲學(xué)家不會(huì)同時(shí)吃飯,也就不會(huì)發(fā)生死鎖欲诺。
領(lǐng)導(dǎo)調(diào)節(jié)
領(lǐng)導(dǎo)定期巡視抄谐,如果發(fā)生了死鎖,就命令某一個(gè)哲學(xué)家放下叉子扰法,先讓別人吃飯蛹含。
7. 實(shí)際開發(fā)中如何避免死鎖?
設(shè)置超時(shí)時(shí)間
使用Concurrent下的ReentrantLock的tryLock(long timeout, TimeUnit unit)
當(dāng)我們使用synchronized的時(shí)候塞颁,如果獲取不到鎖浦箱,是不能退出的,只能繼續(xù)等待直到獲取到這把鎖祠锣。
但是如果我們使用tryLock功能酷窥,設(shè)置超時(shí)時(shí)間(假設(shè)5秒),那么如果等待了5秒后依然沒拿到鎖伴网,超時(shí)后就可以退出做別的事蓬推,防止死鎖。
造成超時(shí)的可能性有很多種澡腾,比如發(fā)生了死鎖沸伏、線程陷入死循環(huán)、線程執(zhí)行很慢等等动分。
當(dāng)我們獲取該鎖失敗的時(shí)候毅糟,我們可以打日志、發(fā)報(bào)警郵件澜公、重啟等等姆另,這些都比造成死鎖要好得多。
如果發(fā)現(xiàn)獲取不到鎖玛瘸,可以退一步海闊天空蜕青,也就是把我當(dāng)前持有的鎖釋放,以便讓行其他線程糊渊,避免死鎖右核。這樣一來,當(dāng)其他線程執(zhí)行完畢后渺绒,自然會(huì)輪到我來繼續(xù)執(zhí)行贺喝,一小步的退讓換來的是海闊天空菱鸥。
代碼演示:TryLockDeadlock類
@Overridepublic void run() {for(int i =0; i <100; i++) {if(flag ==1) {try{if(lock1.tryLock(800, TimeUnit.MILLISECONDS)) {System.out.println("線程1獲取到了鎖1");Thread.sleep(new Random().nextInt(1000));if(lock2.tryLock(800, TimeUnit.MILLISECONDS)) {System.out.println("線程1獲取到了鎖2");System.out.println("線程1成功獲取到了兩把鎖");lock2.unlock();lock1.unlock();break;? ? ? ? ? ? ? ? ? ? ? ? }else{System.out.println("線程1嘗試獲取鎖2失敗,已重試");lock1.unlock();Thread.sleep(new Random().nextInt(1000));? ? ? ? ? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? ? ? }else{System.out.println("線程1獲取鎖1失敗躏鱼,已重試");? ? ? ? ? ? ? ? ? ? }? ? ? ? ? ? ? ? }catch(InterruptedException e) {e.printStackTrace();? ? ? ? ? ? ? ? }? ? ? ? ? ? }復(fù)制代碼
多使用并發(fā)類而不是自己設(shè)計(jì)鎖
盡量使用java.util.concurrent(jdk 1.5以上)包的并發(fā)類代替手寫控制wait,notify并發(fā)
比較常用的是ConcurrentHashMap氮采、ConcurrentLinkedQueue、AtomicBoolean等等染苛,實(shí)際應(yīng)用中
java.util.concurrent.atomic十分有用鹊漠,簡單方便且效率比使用Lock更高。
多用并發(fā)集合少用同步集合 這是另外一個(gè)容易遵循且受益巨大的最佳實(shí)踐茶行,并發(fā)集合比同步集合的可擴(kuò)展性更好躯概,所以在并發(fā)編程時(shí)使用并發(fā)集合效果更好。
如果下一次你需要用到map畔师,你應(yīng)該首先想到用ConcurrentHashMap娶靡。
盡量降低鎖的使用粒度
分別用不同的鎖來保護(hù)同一個(gè)類中多個(gè)獨(dú)立的狀態(tài)變量,而不是對整個(gè)類域只使用一個(gè)鎖看锉。
最低限度的使用同步和鎖姿锭,縮小臨界區(qū)。
如果能使用同步代碼塊伯铣,就不使用同步方法
相對于同步方法我更喜歡同步塊呻此,用同步代碼塊來指定獲取哪個(gè)對象的鎖,這樣就擁有了對鎖的絕對控制權(quán)腔寡。
給你的線程起個(gè)有意義的名字趾诗。
這樣可以方便找bug或追蹤。OrderProcessor, QuoteProcessor or TradeProcessor 這種名字比 Thread-1.Thread-2 and Thread-3 好多了蹬蚁,給線程起一個(gè)和它要完成的任務(wù)相關(guān)的名字恃泪,所有的主要框架甚至JDK都遵循這個(gè)最佳實(shí)踐。
避免鎖的嵌套
例如MustDeadLock類犀斋,就是因?yàn)殒i嵌套導(dǎo)致的死鎖且警。
分配資源前先看能不能收回來
(學(xué)習(xí)銀行家算法的思想)在分配資源前進(jìn)行詳細(xì)計(jì)算雕薪,如果有發(fā)生死鎖的可能就不分配資源,避免死鎖。
盡量不要幾個(gè)功能用同一把鎖
由于一個(gè)線程可以獲得多個(gè)鎖沥潭,比如在一個(gè)同步方法里面調(diào)用另一個(gè)對象的同步方法夹厌,這樣容易獲得多個(gè)鎖肺稀,容易死鎖赘艳,盡量避免。應(yīng)該“專鎖專用”辆脸。
和第一點(diǎn)的區(qū)別:
降低鎖粒度:一個(gè)西瓜切開8塊但校。
專鎖專用:西瓜有西瓜鎖,冬瓜有冬瓜鎖啡氢。
就是說状囱,一個(gè)功能术裸,可能代碼很長,那么不要全部上鎖亭枷,要降低鎖的粒度袭艺。不同的功能,也不要用同一個(gè)鎖叨粘。
廣義上講猾编,這兩點(diǎn)有相似之處,只需要記住原則就是“只鎖需要鎖的部分”就可以升敲,自然就能達(dá)到目的袍镀。
8. 什么是活躍性問題?活鎖冻晤、饑餓和死鎖有什么區(qū)別?
死鎖是最常見的活躍性問題绸吸,不過除了剛才的死鎖之外鼻弧,還有一些類似的問題,會(huì)導(dǎo)致程序無法順利執(zhí)行锦茁,統(tǒng)稱為活躍性問題攘轩。
什么是活鎖
活鎖指的是:雖然線程并沒有阻塞,也始終在運(yùn)行(所以叫做“活”鎖码俩,線程是“活”的)度帮,但是程序卻得不到進(jìn)展,因?yàn)榫€程始終重復(fù)做同樣的事稿存。
活鎖和死鎖的區(qū)別就在于
如果這里死鎖笨篷,那么就是這里兩個(gè)人都始終一動(dòng)不動(dòng),直到對方先抬頭瓣履,他們之間不再說話了率翅,只是等待。
可是如果發(fā)生活鎖袖迎,那么這里的情況就是冕臭,雙方都不停地對對方說“你先起來吧,你先起來吧”燕锥,雙方都一直在說話辜贵,在運(yùn)行,只不過死鎖和活鎖的結(jié)果是一樣的归形,就是誰都不能先抬頭托慨。
工程中的活鎖實(shí)例:消息隊(duì)列
如果某個(gè)消息由于上下游原因,始終處理失敗暇榴,但是失敗時(shí)的處理策略是重試且放到隊(duì)列頭部榴芳,就會(huì)出現(xiàn)活鎖問題:無限重試但永遠(yuǎn)失敗嗡靡。
解決方案是把重試消息放到隊(duì)列尾部,并且設(shè)置重試次數(shù)的上限窟感。
如何解決活鎖問題
我們發(fā)現(xiàn)產(chǎn)生活鎖的最主要原因就是因?yàn)榇蠹抑卦嚨臋C(jī)制是不變的讨彼,比如消息隊(duì)列中,如果失敗也反 復(fù)繼續(xù)重試柿祈,而吃飯的例子中哈误,夫妻發(fā)現(xiàn)對方餓了就一定會(huì)讓對方先吃。
所以在重試策略中躏嚎,我們需要在重試的時(shí)候讓大家不再使用一樣的策略蜜自,例如引入隨機(jī)因素,一個(gè)非常是典型的例子在以太網(wǎng)中有重試機(jī)制卢佣,如果發(fā)生碰撞重荠,那么雙方都會(huì)重試,如果沒有隨機(jī)因素虚茶,那么雙方會(huì)不停在1秒后重發(fā)數(shù)據(jù)包戈鲁,那么又會(huì)碰撞,永無止境嘹叫。
所以以太網(wǎng)協(xié)議里婆殿,發(fā)生碰撞后,等待的時(shí)間并不是固定的罩扇,而是重試時(shí)間最多在 0 ~ 1023 個(gè)時(shí)槽范圍內(nèi)隨機(jī)選擇婆芦,這就是 指數(shù)退避算法,很好地解決了活鎖問題喂饥。
同樣消约,在吃飯的例子中,如果也加入隨機(jī)因素员帮,比如在配偶餓的時(shí)候荆陆,我依然有20%幾率可以先吃,那么也就不會(huì)造成兩個(gè)人都很餓卻都不能吃飯的惡劣局面集侯。
饑餓
當(dāng)線程需要某些資源被啼,但是卻始終得不到,導(dǎo)致線程不能繼續(xù)運(yùn)行棠枉,這種情況就被稱為“饑餓”浓体。
最常見的得不到的資源是CPU資源,例如如果我們把線程的優(yōu)先級設(shè)置得過于低(Java中優(yōu)先級分為1到10)辈讶,或者有某線程持有鎖同時(shí)又無限循環(huán)從而不釋放鎖命浴,或者某程序始終占用某文件的寫鎖,這些情況都可能導(dǎo)致“饑餓”的發(fā)生。
饑餓可能會(huì)導(dǎo)致響應(yīng)性差:比如生闲,我們的瀏覽器有一個(gè)線程負(fù)責(zé)處理前臺(tái)響應(yīng)(打開收藏夾等動(dòng)作)媳溺,另外的后臺(tái)線程負(fù)責(zé)下載圖片和文件、計(jì)算渲染等碍讯。在這種情況下悬蔽,如果后臺(tái)線程把CPU資源都占用了,那么前臺(tái)線程將無法得到很好地執(zhí)行捉兴,這會(huì)導(dǎo)致用戶的體驗(yàn)很差蝎困。