43道多線程面試題,附帶答案(二)

1.線程的sleep()方法和yield()方法有什么區(qū)別?

答: ① sleep()方法給其他線程運(yùn)行機(jī)會(huì)時(shí)不考慮線程的優(yōu)先級(jí),因此會(huì)給低優(yōu)先級(jí)的線程以運(yùn)行的機(jī)會(huì);yield()方法只會(huì)給相同優(yōu)先級(jí)或更高優(yōu)先級(jí)的線程以運(yùn)行的機(jī)會(huì); ② 線程執(zhí)行sleep()方法后轉(zhuǎn)入阻塞(blocked)狀態(tài),而執(zhí)行yield()方法后轉(zhuǎn)入就緒(ready)狀態(tài); ③ sleep()方法聲明拋出InterruptedException,而yield()方法沒有聲明任何異常; ④ sleep()方法比yield()方法(跟操作系統(tǒng)CPU調(diào)度相關(guān))具有更好的可移植性。

讓客戶滿意是我們工作的目標(biāo),不斷超越客戶的期望值來自于我們對(duì)這個(gè)行業(yè)的熱愛。我們立志把好的技術(shù)通過有效、簡(jiǎn)單的方式提供給客戶,將通過不懈努力成為客戶在信息化領(lǐng)域值得信任、有價(jià)值的長期合作伙伴,公司提供的服務(wù)項(xiàng)目有:域名與空間、虛擬空間、營銷軟件、網(wǎng)站建設(shè)、仁布網(wǎng)站維護(hù)、網(wǎng)站推廣。

2.請(qǐng)說出與線程同步以及線程調(diào)度相關(guān)的方法。

答:

  • wait():使一個(gè)線程處于等待(阻塞)狀態(tài),并且釋放所持有的對(duì)象的鎖;

  • sleep():使一個(gè)正在運(yùn)行的線程處于睡眠狀態(tài),是一個(gè)靜態(tài)方法,調(diào)用此方法要處理InterruptedException異常;

  • notify():?jiǎn)拘岩粋€(gè)處于等待狀態(tài)的線程,當(dāng)然在調(diào)用此方法的時(shí)候,并不能確切的喚醒某一個(gè)等待狀態(tài)的線程,而是由JVM確定喚醒哪個(gè)線程,而且與優(yōu)先級(jí)無關(guān);

  • notityAll():?jiǎn)拘阉刑幱诘却隣顟B(tài)的線程,該方法并不是將對(duì)象的鎖給所有線程,而是讓它們競(jìng)爭(zhēng),只有獲得鎖的線程才能進(jìn)入就緒狀態(tài);

3.舉例說明同步和異步。

答:如果系統(tǒng)中存在臨界資源(資源數(shù)量少于競(jìng)爭(zhēng)資源的線程數(shù)量的資源),例如正在寫的數(shù)據(jù)以后可能被另一個(gè)線程讀到,或者正在讀的數(shù)據(jù)可能已經(jīng)被另一個(gè)線程寫過了,那么這些數(shù)據(jù)就必須進(jìn)行同步存?。〝?shù)據(jù)庫操作中的排他鎖就是最好的例子)。當(dāng)應(yīng)用程序在對(duì)象上調(diào)用了一個(gè)需要花費(fèi)很長時(shí)間來執(zhí)行的方法,并且不希望讓程序等待方法的返回時(shí),就應(yīng)該使用異步編程,在很多情況下采用異步途徑往往更有效率。事實(shí)上,所謂的同步就是指阻塞式操作,而異步就是非阻塞式操作。

4.不使用stop停止線程?

當(dāng)run() 或者 call() 方法執(zhí)行完的時(shí)候線程會(huì)自動(dòng)結(jié)束,如果要手動(dòng)結(jié)束一個(gè)線程,你可以用volatile 布爾變量來退出run()方法的循環(huán)或者是取消任務(wù)來中斷線程。

使用自定義的標(biāo)志位決定線程的執(zhí)行情況

public?class?SafeStopThread?implements?Runnable{??
???private?volatile?boolean?stop=false;//此變量必須加上volatile??
???int?a=0;??
???@Override??
????public?void?run()?{??
????????//?TODO?Auto-generated?method?stub??
????????while(!stop){??
???????????????synchronized?("")?{??
????????????????????a++;??
????????????????????try?{??
????????????????????????Thread.sleep(100);??
????????????????????}?catch?(Exception?e)?{??
????????????????????????//?TODO:?handle?exception??
????????????????????}??
????????????????????a--;??
????????????????????String?tn=Thread.currentThread().getName();??
????????????????????System.out.println(tn+":a="+a);??
????????????????}??
????????}??
??????//線程終止??
?????public?void?terminate(){??
?????????stop=true;??
??????}??
??public?static?void?main(String[]?args)?{??
???????SafeStopThread?t=new?SafeStopThread();??
???????Thread?t1=new?Thread(t);??
???????t1.start();??
???????for(int?i=0;i<5;i++){???
???????????new?Thread(t).start();??
???????}??
?????t.terminate();??
???}??
}

5.Java中如何實(shí)現(xiàn)線程?各有什么優(yōu)缺點(diǎn),比較常用的是那種,為什么?

在語言層面有兩種方式。java.lang.Thread 類的實(shí)例就是一個(gè)線程但是它需要調(diào)用java.lang.Runnable接口來執(zhí)行,由于線程類本身就是調(diào)用的Runnable接口所以你可以繼承java.lang.Thread 類或者直接調(diào)用Runnable接口來重寫run()方法實(shí)現(xiàn)線程。

Java不支持類的多重繼承,但允許你調(diào)用多個(gè)接口。所以如果你要繼承其他類,當(dāng)然是調(diào)用Runnable接口好了。

6.如何控制某個(gè)方法允許并發(fā)訪問線程的大小?

Semaphore兩個(gè)重要的方法就是semaphore.acquire() 請(qǐng)求一個(gè)信號(hào)量,這時(shí)候的信號(hào)量個(gè)數(shù)-1(一旦沒有可使用的信號(hào)量,也即信號(hào)量個(gè)數(shù)變?yōu)樨?fù)數(shù)時(shí),再次請(qǐng)求的時(shí)候就會(huì)阻塞,直到其他線程釋放了信號(hào)量)semaphore.release()釋放一個(gè)信號(hào)量,此時(shí)信號(hào)量個(gè)數(shù)+1

public?class?SemaphoreTest?{??
????private?Semaphore?mSemaphore?=?new?Semaphore(5);??
????public?void?run(){??
????????for(int?i=0;?i<?100;?i++){??
????????????new?Thread(new?Runnable()?{??
????????????????@Override??
????????????????public?void?run()?{??
????????????????????test();??
????????????????}??
????????????}).start();??
????????}??
????}??
??
????private?void?test(){??
????????try?{??
????????????mSemaphore.acquire();??
????????}?catch?(InterruptedException?e)?{??
????????????e.printStackTrace();??
????????}??
????????System.out.println(Thread.currentThread().getName()?+?"?進(jìn)來了");??
????????try?{??
????????????Thread.sleep(1000);??
????????}?catch?(InterruptedException?e)?{??
????????????e.printStackTrace();??
????????}??
????????System.out.println(Thread.currentThread().getName()?+?"?出去了");??
????????mSemaphore.release();??
????}??
}

7.在Java中什么是線程調(diào)度?

線程調(diào)度是指系統(tǒng)為線程分配處理器使用權(quán)的過程。 主要調(diào)度方式有兩種,分別是協(xié)同式線程調(diào)度和搶占式線程調(diào)度。

協(xié)同式線程調(diào)度:線程的執(zhí)行時(shí)間由線程本身控制,當(dāng)線程把自己的工作執(zhí)行完了之后,主動(dòng)通知系統(tǒng)切換到另一個(gè)線程上。

  • 好處是切換操作對(duì)于線程自己是可知的,沒什么線程同步問題。

  • 壞處是線程執(zhí)行時(shí)間不可控,可能會(huì)一直阻塞然后系統(tǒng)崩潰。

搶占式線程調(diào)度:每個(gè)線程由系統(tǒng)分配執(zhí)行時(shí)間,不由線程本身決定。線程的執(zhí)行時(shí)間是系統(tǒng)可控的,不會(huì)有一直阻塞的問題。

Java使用搶占式調(diào)度

8.Java中用到的線程調(diào)度算法是什么?

搶占式。一個(gè)線程用完CPU之后,操作系統(tǒng)會(huì)根據(jù)線程優(yōu)先級(jí)、線程饑餓情況等數(shù)據(jù)算出一個(gè)總的優(yōu)先級(jí)并分配下一個(gè)時(shí)間片給某個(gè)線程執(zhí)行。

9.線程類的構(gòu)造方法、靜態(tài)塊是被哪個(gè)線程調(diào)用的?

線程類的構(gòu)造方法、靜態(tài)塊是被new這個(gè)線程類所在的線程所調(diào)用的,而run方法里面的代碼才是被線程自身所調(diào)用的。

10.在實(shí)現(xiàn)Runnable的接口中怎么樣訪問當(dāng)前線程對(duì)象,比如拿到當(dāng)前線程的名字?

Thread?t?=?Thread.currentThread();
String?name?=?t.getName();
System.out.println("name="?+?name);

11.什么是線程池?為什么要使用它?為什么使用Executor框架比使用應(yīng)用創(chuàng)建和管理線程好?

創(chuàng)建線程要花費(fèi)昂貴的資源和時(shí)間,如果任務(wù)來了才創(chuàng)建線程那么響應(yīng)時(shí)間會(huì)變長,而且一個(gè)進(jìn)程能創(chuàng)建的線程數(shù)有限。

為了避免這些問題,在程序啟動(dòng)的時(shí)候就創(chuàng)建若干線程來響應(yīng)處理,它們被稱為線程池,里面的線程叫工作線程。

Executor框架讓你可以創(chuàng)建不同的線程池。比如單線程池,每次處理一個(gè)任務(wù);數(shù)目固定的線程池或者是緩存線程池(一個(gè)適合很多生存期短的任務(wù)的程序的可擴(kuò)展線程池)。

12常用的線程池模式以及不同線程池的使用場(chǎng)景?

以下是Java自帶的幾種線程池: 1、newFixedThreadPool 創(chuàng)建一個(gè)指定工作線程數(shù)量的線程池。 每當(dāng)提交一個(gè)任務(wù)就創(chuàng)建一個(gè)工作線程,如果工作線程數(shù)量達(dá)到線程池初始的最大數(shù),則將提交的任務(wù)存入到池隊(duì)列中。

2、newCachedThreadPool 創(chuàng)建一個(gè)可緩存的線程池。 這種類型的線程池特點(diǎn)是:

  • 1).工作線程的創(chuàng)建數(shù)量幾乎沒有限制(其實(shí)也有限制的,數(shù)目為Interger. MAX_VALUE),這樣可靈活的往線程池中添加線程。

  • 2).如果長時(shí)間沒有往線程池中提交任務(wù),即如果工作線程空閑了指定的時(shí)間(默認(rèn)為1分鐘),則該工作線程將自動(dòng)終止。終止后,如果你又提交了新的任務(wù),則線程池重新創(chuàng)建一個(gè)工作線程。

3、newSingleThreadExecutor創(chuàng)建一個(gè)單線程化的Executor,即只創(chuàng)建唯一的工作者線程來執(zhí)行任務(wù),如果這個(gè)線程異常結(jié)束,會(huì)有另一個(gè)取代它,保證順序執(zhí)行(我覺得這點(diǎn)是它的特色)。

單工作線程最大的特點(diǎn)是可保證順序地執(zhí)行各個(gè)任務(wù),并且在任意給定的時(shí)間不會(huì)有多個(gè)線程是活動(dòng)的。

4、newScheduleThreadPool 創(chuàng)建一個(gè)定長的線程池,而且支持定時(shí)的以及周期性的任務(wù)執(zhí)行,類似于Timer。

13.在Java中Executor、ExecutorService、Executors的區(qū)別?

Executor 和 ExecutorService 這兩個(gè)接口主要的區(qū)別是:

  • ExecutorService 接口繼承了 Executor 接口,是 Executor 的子接口

  • Executor 和 ExecutorService 第二個(gè)區(qū)別是:Executor 接口定義了 execute()方法用來接收一個(gè)Runnable接口的對(duì)象,而 ExecutorService 接口中的 submit()方法可以接受Runnable和Callable接口的對(duì)象。

  • Executor 和 ExecutorService 接口第三個(gè)區(qū)別是 Executor 中的 execute() 方法不返回任何結(jié)果,而 ExecutorService 中的 submit()方法可以通過一個(gè) Future 對(duì)象返回運(yùn)算結(jié)果。

  • Executor 和 ExecutorService 接口第四個(gè)區(qū)別是除了允許客戶端提交一個(gè)任務(wù),ExecutorService 還提供用來控制線程池的方法。比如:調(diào)用 shutDown() 方法終止線程池。

Executors 類提供工廠方法用來創(chuàng)建不同類型的線程池。

比如: newSingleThreadExecutor() 創(chuàng)建一個(gè)只有一個(gè)線程的線程池,newFixedThreadPool(int numOfThreads)來創(chuàng)建固定線程數(shù)的線程池,newCachedThreadPool()可以根據(jù)需要?jiǎng)?chuàng)建新的線程,但如果已有線程是空閑的會(huì)重用已有線程。

14.如何創(chuàng)建一個(gè)Java線程池?

Java通過Executors提供四種線程池,分別為:

newCachedThreadPool創(chuàng)建一個(gè)可緩存線程池,如果線程池長度超過處理需要,可靈活回收空閑線程,若無可回收,則新建線程。

newFixedThreadPool 創(chuàng)建一個(gè)定長線程池,可控制線程最大并發(fā)數(shù),超出的線程會(huì)在隊(duì)列中等待。

newScheduledThreadPool 創(chuàng)建一個(gè)定長線程池,支持定時(shí)及周期性任務(wù)執(zhí)行。

newSingleThreadExecutor 創(chuàng)建一個(gè)單線程化的線程池,它只會(huì)用唯一的工作線程來執(zhí)行任務(wù),保證所有任務(wù)按照指定順序(FIFO, LIFO, 優(yōu)先級(jí))執(zhí)行。

15.Thread 類中的start() 和 run() 方法有什么區(qū)別?

start()方法被用來啟動(dòng)新創(chuàng)建的線程,而且start()內(nèi)部調(diào)用了run()方法,這和直接調(diào)用run()方法的效果不一樣。

當(dāng)你調(diào)用run()方法的時(shí)候,只會(huì)是在原來的線程中調(diào)用,沒有新的線程啟動(dòng),start()方法才會(huì)啟動(dòng)新線程。

16.Java線程池中submit() 和 execute()方法有什么區(qū)別?

兩個(gè)方法都可以向線程池提交任務(wù),execute()方法的返回類型是void,它定義在Executor接口中, 而submit()方法可以返回持有計(jì)算結(jié)果的Future對(duì)象,它定義在ExecutorService接口中,它擴(kuò)展了Executor接口,其它線程池類像ThreadPoolExecutor和ScheduledThreadPoolExecutor都有這些方法。

17.Java中notify 和 notifyAll有什么區(qū)別?

notify()方法不能喚醒某個(gè)具體的線程,所以只有一個(gè)線程在等待的時(shí)候它才有用武之地。而notifyAll()喚醒所有線程并允許他們爭(zhēng)奪鎖確保了至少有一個(gè)線程能繼續(xù)運(yùn)行。

當(dāng)有線程調(diào)用了對(duì)象的 notifyAll()方法(喚醒所有 wait 線程)或 notify()方法(只隨機(jī)喚醒一個(gè) wait 線程),被喚醒的的線程便會(huì)進(jìn)入該對(duì)象的鎖池中,鎖池中的線程會(huì)去競(jìng)爭(zhēng)該對(duì)象鎖。也就是說,調(diào)用了notify后只要一個(gè)線程會(huì)由等待池進(jìn)入鎖池,而notifyAll會(huì)將該對(duì)象等待池內(nèi)的所有線程移動(dòng)到鎖池中,等待鎖競(jìng)爭(zhēng)

優(yōu)先級(jí)高的線程競(jìng)爭(zhēng)到對(duì)象鎖的概率大,假若某線程沒有競(jìng)爭(zhēng)到該對(duì)象鎖,它還會(huì)留在鎖池中,唯有線程再次調(diào)用 wait()方法,它才會(huì)重新回到等待池中。

18.為什么wait, notify 和 notifyAll這些方法不在thread類里面?

一個(gè)很明顯的原因是JAVA提供的鎖是對(duì)象級(jí)的而不是線程級(jí)的,每個(gè)對(duì)象都有鎖,通過線程獲得。

如果線程需要等待某些鎖那么調(diào)用對(duì)象中的wait()方法就有意義了。如果wait()方法定義在Thread類中,線程正在等待的是哪個(gè)鎖就不明顯了。

簡(jiǎn)單的說,由于wait,notify和notifyAll都是鎖級(jí)別的操作,所以把他們定義在Object類中因?yàn)殒i屬于對(duì)象。

19.為什么wait和notify方法要在同步塊中調(diào)用?

主要是因?yàn)镴ava API強(qiáng)制要求這樣做,如果你不這么做,你的代碼會(huì)拋出IllegalMonitorStateException異常。還有一個(gè)原因是為了避免wait和notify之間產(chǎn)生競(jìng)態(tài)條件。

最主要的原因是為了防止以下這種情況

//?等待者(Thread1)while?(condition?!=?true)?{?//?step.1
????lock.wait()?//?step.4}//?喚醒者(Thread2)condition?=?true;?//?step.2lock.notify();?//?step.3

在對(duì)之前的代碼去掉 synchronized 塊之后,如果在等待者判斷 condition != true 之后而調(diào)用 wait() 之前,喚醒者**將 condition 修改成了 true 同時(shí)調(diào)用了 notify() **的話,那么等待者在調(diào)用了 wait() 之后就沒有機(jī)會(huì)被喚醒了。

20.講下join,yield方法的作用,以及什么場(chǎng)合用它們?

join() 的作用:讓“主線程”等待“子線程”結(jié)束之后才能繼續(xù)運(yùn)行。

yield方法可以暫停當(dāng)前正在執(zhí)行的線程對(duì)象,讓其它有相同優(yōu)先級(jí)的線程執(zhí)行。它是一個(gè)靜態(tài)方法而且只保證當(dāng)前線程放棄CPU占用而不能保證使其它線程一定能占用CPU,執(zhí)行yield()的線程有可能在進(jìn)入到暫停狀態(tài)后馬上又被執(zhí)行。

21.sleep方法有什么作用,一般用來做什么?

sleep()方法(休眠)是線程類(Thread)的靜態(tài)方法,調(diào)用此方法會(huì)讓當(dāng)前線程暫停執(zhí)行指定的時(shí)間,將執(zhí)行機(jī)會(huì)(CPU)讓給其他線程,但是對(duì)象的鎖依然保持,因此休眠時(shí)間結(jié)束后會(huì)自動(dòng)恢復(fù)。注意這里的恢復(fù)并不是恢復(fù)到執(zhí)行的狀態(tài),而是恢復(fù)到可運(yùn)行狀態(tài)中等待CPU的寵幸。

22.Java多線程中調(diào)用wait() 和 sleep()方法有什么不同?

Java程序中wait和sleep都會(huì)造成某種形式的暫停,它們可以滿足不同的需要。

  • wait存在于Object類中;sleep存在于Thread類中。

  • wait會(huì)讓出CPU資源以及釋放鎖;sleep只會(huì)釋放CPU資源。

  • wait只能在同步塊中使用;sleep沒這限制。

  • wait需要notify(或 notifyAll)喚醒,進(jìn)入等鎖狀態(tài);sleep到指定時(shí)間便會(huì)自動(dòng)恢復(fù)到運(yùn)行狀態(tài)。

23.為什么Thread里面的大部分方法都是final的?

不能被重寫,線程的很多方法都是由系統(tǒng)調(diào)用的,不能通過子類覆寫去改變他們的行為。

24.為什么Thread類的sleep()和yield()方法是靜態(tài)的?

Thread類的sleep()和yield()方法將在當(dāng)前正在執(zhí)行的線程上運(yùn)行。

該代碼只有在某個(gè)A線程執(zhí)行時(shí)會(huì)被執(zhí)行,這種情況下通知某個(gè)B線程yield是無意義的(因?yàn)锽線程本來就沒在執(zhí)行)。因此只有當(dāng)前線程執(zhí)行yield才是有意義的。通過使該方法為static,你將不會(huì)浪費(fèi)時(shí)間嘗試yield 其他線程。

只能給自己喂安眠藥,不能給別人喂安眠藥。

25.什么是阻塞式方法?

阻塞式方法是指程序會(huì)一直等待該方法完成期間不做其他事情。

ServerSocket的accept()方法就是一直等待客戶端連接。這里的阻塞是指調(diào)用結(jié)果返回之前,當(dāng)前線程會(huì)被掛起,直到得到結(jié)果之后才會(huì)返回。

此外,還有異步和非阻塞式方法在任務(wù)完成前就返回。

26.如何強(qiáng)制啟動(dòng)一個(gè)線程?

在Java里面沒有辦法強(qiáng)制啟動(dòng)一個(gè)線程,它是被線程調(diào)度器控制著

27.一個(gè)線程運(yùn)行時(shí)發(fā)生異常會(huì)怎樣?

簡(jiǎn)單的說,如果異常沒有被捕獲該線程將會(huì)停止執(zhí)行。

Thread.UncaughtExceptionHandler是用于處理未捕獲異常造成線程突然中斷情況的一個(gè)內(nèi)嵌接口。

當(dāng)一個(gè)未捕獲異常將造成線程中斷的時(shí)候JVM會(huì)使用Thread.getUncaughtExceptionHandler()來查詢線程的UncaughtExceptionHandler并將線程和異常作為參數(shù)傳遞給handler的uncaughtException()方法進(jìn)行處理。

28.在線程中你怎么處理不可控制異常?

在Java中有兩種異常。

非運(yùn)行時(shí)異常(Checked Exception):這種異常必須在方法聲明的throws語句指定,或者在方法體內(nèi)捕獲。例如:IOException和ClassNotFoundException。

運(yùn)行時(shí)異常(Unchecked Exception):這種異常不必在方法聲明中指定,也不需要在方法體中捕獲。例如,NumberFormatException。

因?yàn)閞un()方法不支持throws語句,所以當(dāng)線程對(duì)象的run()方法拋出非運(yùn)行異常時(shí),我們必須捕獲并且處理它們。當(dāng)運(yùn)行時(shí)異常從run()方法中拋出時(shí),默認(rèn)行為是在控制臺(tái)輸出堆棧記錄并且退出程序。

好在,java提供給我們一種在線程對(duì)象里捕獲和處理運(yùn)行時(shí)異常的一種機(jī)制。實(shí)現(xiàn)用來處理運(yùn)行時(shí)異常的類,這個(gè)類實(shí)現(xiàn)UncaughtExceptionHandler接口并且實(shí)現(xiàn)這個(gè)接口的uncaughtException()方法。示例:

package?concurrency;

import?java.lang.Thread.UncaughtExceptionHandler;

public?class?Main2?{
????public?static?void?main(String[]?args)?{
????????Task?task?=?new?Task();
????????Thread?thread?=?new?Thread(task);
????????thread.setUncaughtExceptionHandler(new?ExceptionHandler());
????????thread.start();
????}
}

class?Task?implements?Runnable{
????@Override
????public?void?run()?{
????????int?numero?=?Integer.parseInt("TTT");
????}
}

class?ExceptionHandler?implements?UncaughtExceptionHandler{
????@Override
????public?void?uncaughtException(Thread?t,?Throwable?e)?{
????????System.out.printf("An?exception?has?been?captured\n");
????????System.out.printf("Thread:??%s\n",?t.getId());
????????System.out.printf("Exception:??%s:??%s\n",?e.getClass().getName(),e.getMessage());
????????System.out.printf("Stack?Trace:??\n");
????????e.printStackTrace(System.out);
????????System.out.printf("Thread?status:??%s\n",t.getState());
????}
}

當(dāng)一個(gè)線程拋出了異常并且沒有被捕獲時(shí)(這種情況只可能是運(yùn)行時(shí)異常),JVM檢查這個(gè)線程是否被預(yù)置了未捕獲異常處理器。如果找到,JVM將調(diào)用線程對(duì)象的這個(gè)方法,并將線程對(duì)象和異常作為傳入?yún)?shù)。

Thread類還有另一個(gè)方法可以處理未捕獲到的異常,即靜態(tài)方法setDefaultUncaughtExceptionHandler()。這個(gè)方法在應(yīng)用程序中為所有的線程對(duì)象創(chuàng)建了一個(gè)異常處理器。

當(dāng)線程拋出一個(gè)未捕獲到的異常時(shí),JVM將為異常尋找以下三種可能的處理器。

  • 首先,它查找線程對(duì)象的未捕獲異常處理器。

  • 如果找不到,JVM繼續(xù)查找線程對(duì)象所在的線程組(ThreadGroup)的未捕獲異常處理器。

  • 如果還是找不到,如同本節(jié)所講的,JVM將繼續(xù)查找默認(rèn)的未捕獲異常處理器。

  • 如果沒有一個(gè)處理器存在,JVM則將堆棧異常記錄打印到控制臺(tái),并退出程序。

29.為什么你應(yīng)該在循環(huán)中檢查等待條件?

處于等待狀態(tài)的線程可能會(huì)收到錯(cuò)誤警報(bào)和偽喚醒,如果不在循環(huán)中檢查等待條件,程序就會(huì)在沒有滿足結(jié)束條件的情況下退出。

1、一般來說,wait肯定是在某個(gè)條件調(diào)用的,不是if就是while 2、放在while里面,是防止出于waiting的對(duì)象被別的原因調(diào)用了喚醒方法,但是while里面的條件并沒有滿足(也可能當(dāng)時(shí)滿足了,但是由于別的線程操作后,又不滿足了),就需要再次調(diào)用wait將其掛起。 3、其實(shí)還有一點(diǎn),就是while最好也被同步,這樣不會(huì)導(dǎo)致錯(cuò)失信號(hào)。

while(condition){????wait();
}

30.多線程中的忙循環(huán)是什么?

忙循環(huán)就是程序員用循環(huán)讓一個(gè)線程等待,不像傳統(tǒng)方法wait()、 sleep() 或 yield(),它們都放棄了CPU控制,而忙循環(huán)不會(huì)放棄CPU,它就是在運(yùn)行一個(gè)空循環(huán)。

這么做的目的是為了保留CPU緩存,在多核系統(tǒng)中,一個(gè)等待線程醒來的時(shí)候可能會(huì)在另一個(gè)內(nèi)核運(yùn)行,這樣會(huì)重建緩存。為了避免重建緩存和減少等待重建的時(shí)間就可以使用它了。

31.什么是自旋鎖?

沒有獲得鎖的線程一直循環(huán)在那里看是否該鎖的保持者已經(jīng)釋放了鎖,這就是自旋鎖。

32.什么是互斥鎖?

互斥鎖:從等待到解鎖過程,線程會(huì)從sleep狀態(tài)變?yōu)閞unning狀態(tài),過程中有線程上下文的切換,搶占CPU等開銷。

33.自旋鎖的優(yōu)缺點(diǎn)?

自旋鎖不會(huì)引起調(diào)用者休眠,如果自旋鎖已經(jīng)被別的線程保持,調(diào)用者就一直循環(huán)在那里看是否該自旋鎖的保持者釋放了鎖。由于自旋鎖不會(huì)引起調(diào)用者休眠,所以自旋鎖的效率遠(yuǎn)高于互斥鎖。

雖然自旋鎖效率比互斥鎖高,但它會(huì)存在下面兩個(gè)問題: 1、自旋鎖一直占用CPU,在未獲得鎖的情況下,一直運(yùn)行,如果不能在很短的時(shí)間內(nèi)獲得鎖,會(huì)導(dǎo)致CPU效率降低。 2、試圖遞歸地獲得自旋鎖會(huì)引起死鎖。遞歸程序決不能在持有自旋鎖時(shí)調(diào)用它自己,也決不能在遞歸調(diào)用時(shí)試圖獲得相同的自旋鎖。

由此可見,我們要慎重的使用自旋鎖,自旋鎖適合于鎖使用者保持鎖時(shí)間比較短并且鎖競(jìng)爭(zhēng)不激烈的情況。正是由于自旋鎖使用者一般保持鎖時(shí)間非常短,因此選擇自旋而不是睡眠是非常必要的,自旋鎖的效率遠(yuǎn)高于互斥鎖。

34.如何在兩個(gè)線程間共享數(shù)據(jù)?

同一個(gè)Runnable,使用全局變量。

第一種:將共享數(shù)據(jù)封裝到一個(gè)對(duì)象中,把這個(gè)共享數(shù)據(jù)所在的對(duì)象傳遞給不同的Runnable

第二種:將這些Runnable對(duì)象作為某一個(gè)類的內(nèi)部類,共享的數(shù)據(jù)作為外部類的成員變量,對(duì)共享數(shù)據(jù)的操作分配給外部類的方法來完成,以此實(shí)現(xiàn)對(duì)操作共享數(shù)據(jù)的互斥和通信,作為內(nèi)部類的Runnable來操作外部類的方法,實(shí)現(xiàn)對(duì)數(shù)據(jù)的操作

class?ShareData?{
?private?int?x?=?0;

?public?synchronized?void?addx(){
???x++;
???System.out.println("x++?:?"+x);
?}
?public?synchronized?void?subx(){
???x--;
???System.out.println("x--?:?"+x);
?}
}

public?class?ThreadsVisitData?{
?
?public?static?ShareData?share?=?new?ShareData();
?
?public?static?void?main(String[]?args)?{
??//final?ShareData?share?=?new?ShareData();
??new?Thread(new?Runnable()?{
????public?void?run()?{
????????for(int?i?=?0;i<100;i++){
????????????share.addx();
????????}
????}
??}).start();
??new?Thread(new?Runnable()?{
????public?void?run()?{
????????for(int?i?=?0;i<100;i++){
????????????share.subx();
????????}
????}
???}).start();?
?}
}

35Java中Runnable和Callable有什么不同?

Runnable和Callable都是接口, 不同之處: 1.Callable可以返回一個(gè)類型V,而Runnable不可以 2.Callable能夠拋出checked exception,而Runnable不可以。 3.Runnable是自從java1.1就有了,而Callable是1.5之后才加上去的 4.Callable和Runnable都可以應(yīng)用于executors。而Thread類只支持Runnable.

import?java.util.concurrent.Callable;??
import?java.util.concurrent.ExecutionException;??
import?java.util.concurrent.ExecutorService;??
import?java.util.concurrent.Executors;??
import?java.util.concurrent.Future;??
??
public?class?ThreadTestB?{??
????public?static?void?main(String[]?args)?{??
????????ExecutorService?e=Executors.newFixedThreadPool(10);??
????????Future?f1=e.submit(new?MyCallableA());??
????????Future?f2=e.submit(new?MyCallableA());??
????????Future?f3=e.submit(new?MyCallableA());????????
????????System.out.println("--Future.get()....");??
????????try?{??
????????????System.out.println(f1.get());??
????????????System.out.println(f2.get());??
????????????System.out.println(f3.get());????????????
????????}?catch?(InterruptedException?e1)?{??
????????????e1.printStackTrace();??
????????}?catch?(ExecutionException?e1)?{??
????????????e1.printStackTrace();??
????????}??
????????e.shutdown();??
????}??
}??
??
class?MyCallableA?implements?Callable<String>{??
????public?String?call()?throws?Exception?{??
????????System.out.println("開始執(zhí)行Callable");??
????????String[]?ss={"zhangsan","lisi"};??
????????long[]?num=new?long[2];??
????????for(int?i=0;i<1000000;i++){??
????????????num[(int)(Math.random()*2)]++;??
????????}??
??????????
????????if(num[0]>num[1]){??
????????????return?ss[0];??
????????}else?if(num[0]<num[1]){??
????????????throw?new?Exception("棄權(quán)!");??
????????}else{??
????????????return?ss[1];??
????????}??
????}?
}

36.Java中CyclicBarrier 和 CountDownLatch有什么不同?

CountDownLatch和CyclicBarrier都能夠?qū)崿F(xiàn)線程之間的等待,只不過它們側(cè)重點(diǎn)不同:

  • CountDownLatch一般用于某個(gè)線程A等待若干個(gè)其他線程執(zhí)行完任務(wù)之后,它才執(zhí)行;

  • CyclicBarrier一般用于一組線程互相等待至某個(gè)狀態(tài),然后這一組線程再同時(shí)執(zhí)行;

  • 另外,CountDownLatch是不能夠重用的,而CyclicBarrier是可以重用的。

CountDownLatch的用法:

public?class?Test?{
?????public?static?void?main(String[]?args)?{???
?????????final?CountDownLatch?latch?=?new?CountDownLatch(2);
??????????
?????????new?Thread(){
?????????????public?void?run()?{
?????????????????try?{
?????????????????????System.out.println("子線程"+Thread.currentThread().getName()+"正在執(zhí)行");
????????????????????Thread.sleep(3000);
????????????????????System.out.println("子線程"+Thread.currentThread().getName()+"執(zhí)行完畢");
????????????????????latch.countDown();
????????????????}?catch?(InterruptedException?e)?{
????????????????????e.printStackTrace();
????????????????}
?????????????};
?????????}.start();
??????????
?????????new?Thread(){
?????????????public?void?run()?{
?????????????????try?{
?????????????????????System.out.println("子線程"+Thread.currentThread().getName()+"正在執(zhí)行");
?????????????????????Thread.sleep(3000);
?????????????????????System.out.println("子線程"+Thread.currentThread().getName()+"執(zhí)行完畢");
?????????????????????latch.countDown();
????????????????}?catch?(InterruptedException?e)?{
????????????????????e.printStackTrace();
????????????????}
?????????????};
?????????}.start();
??????????
?????????try?{
?????????????System.out.println("等待2個(gè)子線程執(zhí)行完畢...");
????????????latch.await();
????????????System.out.println("2個(gè)子線程已經(jīng)執(zhí)行完畢");
????????????System.out.println("繼續(xù)執(zhí)行主線程");
????????}?catch?(InterruptedException?e)?{
????????????e.printStackTrace();
????????}
?????}
}

CyclicBarrier用法:

public?class?Test?{
????public?static?void?main(String[]?args)?{
????????int?N?=?4;
????????CyclicBarrier?barrier??=?new?CyclicBarrier(N,new?Runnable()?{
????????????@Override
????????????public?void?run()?{
????????????????System.out.println("當(dāng)前線程"+Thread.currentThread().getName());???
????????????}
????????});
?????????
????????for(int?i=0;i<N;i++)
????????????new?Writer(barrier).start();
????}
????static?class?Writer?extends?Thread{
????????private?CyclicBarrier?cyclicBarrier;
????????public?Writer(CyclicBarrier?cyclicBarrier)?{
????????????this.cyclicBarrier?=?cyclicBarrier;
????????}
?
????????@Override
????????public?void?run()?{
????????????System.out.println("線程"+Thread.currentThread().getName()+"正在寫入數(shù)據(jù)...");
????????????try?{
????????????????Thread.sleep(5000);??????//以睡眠來模擬寫入數(shù)據(jù)操作
????????????????System.out.println("線程"+Thread.currentThread().getName()+"寫入數(shù)據(jù)完畢,等待其他線程寫入完畢");
????????????????cyclicBarrier.await();
????????????}?catch?(InterruptedException?e)?{
????????????????e.printStackTrace();
????????????}catch(BrokenBarrierException?e){
????????????????e.printStackTrace();
????????????}
????????????System.out.println("所有線程寫入完畢,繼續(xù)處理其他任務(wù)...");
????????}
????}
}

37.Java中interrupted和isInterruptedd方法的區(qū)別?

interrupt方法用于中斷線程。調(diào)用該方法的線程的狀態(tài)為將被置為”中斷”狀態(tài)。

注意:線程中斷僅僅是置線程的中斷狀態(tài)位,不會(huì)停止線程。需要用戶自己去監(jiān)視線程的狀態(tài)為并做處理。支持線程中斷的方法(也就是線程中斷后會(huì)拋出interruptedException的方法)就是在監(jiān)視線程的中斷狀態(tài),一旦線程的中斷狀態(tài)被置為“中斷狀態(tài)”,就會(huì)拋出中斷異常。

isInterrupted 只是簡(jiǎn)單的查詢中斷狀態(tài),不會(huì)對(duì)狀態(tài)進(jìn)行修改。

38.concurrentHashMap的源碼理解以及內(nèi)部實(shí)現(xiàn)原理,為什么他是同步的且效率高

ConcurrentHashMap 分析

ConcurrentHashMap的結(jié)構(gòu)是比較復(fù)雜的,都深究去本質(zhì),其實(shí)也就是數(shù)組和鏈表而已。我們由淺入深慢慢的分析其結(jié)構(gòu)。

先簡(jiǎn)單分析一下,ConcurrentHashMap 的成員變量中,包含了一個(gè) Segment 的數(shù)組(final Segment<K,V>[] segments;),而 Segment 是 ConcurrentHashMap 的內(nèi)部類,然后在 Segment 這個(gè)類中,包含了一個(gè) HashEntry 的數(shù)組(transient volatile HashEntry<K,V>[] table;)。而 HashEntry 也是ConcurrentHashMap 的內(nèi)部類。HashEntry 中,包含了 key 和 value 以及 next 指針(類似于 HashMap 中 Entry),所以 HashEntry 可以構(gòu)成一個(gè)鏈表。

所以通俗的講,ConcurrentHashMap 數(shù)據(jù)結(jié)構(gòu)為一個(gè) Segment 數(shù)組,Segment 的數(shù)據(jù)結(jié)構(gòu)為 HashEntry 的數(shù)組,而 HashEntry 存的是我們的鍵值對(duì),可以構(gòu)成鏈表。

首先,我們看一下 HashEntry 類。

HashEntry

HashEntry 用來封裝散列映射表中的鍵值對(duì)。在 HashEntry 類中,key,hash 和 next 域都被聲明為 final 型,value 域被聲明為 volatile 型。其類的定義為:

static?final?class?HashEntry<K,V>?{
????????final?int?hash;
????????final?K?key;
????????volatile?V?value;
????????volatile?HashEntry<K,V>?next;

????????HashEntry(int?hash,?K?key,?V?value,?HashEntry<K,V>?next)?{
????????????this.hash?=?hash;
????????????this.key?=?key;
????????????this.value?=?value;
????????????this.next?=?next;
????????}
????????...
????????...
}

HashEntry 的學(xué)習(xí)可以類比著 HashMap 中的 Entry。我們的存儲(chǔ)鍵值對(duì)的過程中,散列的時(shí)候如果發(fā)生“碰撞”,將采用“分離鏈表法”來處理碰撞:把碰撞的 HashEntry 對(duì)象鏈接成一個(gè)鏈表。

如下圖,我們?cè)谝粋€(gè)空桶中插入 A、B、C 兩個(gè) HashEntry 對(duì)象后的結(jié)構(gòu)圖(其實(shí)應(yīng)該為鍵值對(duì),在這進(jìn)行了簡(jiǎn)化以方便更容易理解):

43道多線程面試題,附帶答案(二)

Segment

Segment 的類定義為static final class Segment<K,V> extends ReentrantLock implements Serializable。其繼承于 ReentrantLock 類,從而使得 Segment 對(duì)象可以充當(dāng)鎖的角色。Segment 中包含HashEntry 的數(shù)組,其可以守護(hù)其包含的若干個(gè)桶(HashEntry的數(shù)組)。Segment 在某些意義上有點(diǎn)類似于 HashMap了,都是包含了一個(gè)數(shù)組,而數(shù)組中的元素可以是一個(gè)鏈表。

table:table 是由 HashEntry 對(duì)象組成的數(shù)組如果散列時(shí)發(fā)生碰撞,碰撞的 HashEntry 對(duì)象就以鏈表的形式鏈接成一個(gè)鏈表table數(shù)組的數(shù)組成員代表散列映射表的一個(gè)桶每個(gè) table 守護(hù)整個(gè) ConcurrentHashMap 包含桶總數(shù)的一部分如果并發(fā)級(jí)別為 16,table 則守護(hù) ConcurrentHashMap 包含的桶總數(shù)的 1/16。

count 變量是計(jì)算器,表示每個(gè) Segment 對(duì)象管理的 table 數(shù)組(若干個(gè) HashEntry 的鏈表)包含的HashEntry 對(duì)象的個(gè)數(shù)。之所以在每個(gè)Segment對(duì)象中包含一個(gè) count 計(jì)數(shù)器,而不在 ConcurrentHashMap 中使用全局的計(jì)數(shù)器,是為了避免出現(xiàn)“熱點(diǎn)域”而影響并發(fā)性。

/**
?*?Segments?are?specialized?versions?of?hash?tables.??This
?*?subclasses?from?ReentrantLock?opportunistically,?just?to
?*?simplify?some?locking?and?avoid?separate?construction.
?*/
static?final?class?Segment<K,V>?extends?ReentrantLock?implements?Serializable?{
????/**
?????*?The?per-segment?table.?Elements?are?accessed?via
?????*?entryAt/setEntryAt?providing?volatile?semantics.
?????*/
????transient?volatile?HashEntry<K,V>[]?table;

????/**
?????*?The?number?of?elements.?Accessed?only?either?within?locks
?????*?or?among?other?volatile?reads?that?maintain?visibility.
?????*/
????transient?int?count;
????transient?int?modCount;
????/**
?????*?裝載因子
?????*/
????final?float?loadFactor;
}

我們通過下圖來展示一下插入 ABC 三個(gè)節(jié)點(diǎn)后,Segment 的示意圖:

43道多線程面試題,附帶答案(二)

其實(shí)從我個(gè)人角度來說,Segment結(jié)構(gòu)是與HashMap很像的。

ConcurrentHashMap

ConcurrentHashMap 的結(jié)構(gòu)中包含的 Segment 的數(shù)組,在默認(rèn)的并發(fā)級(jí)別會(huì)創(chuàng)建包含 16 個(gè) Segment 對(duì)象的數(shù)組。通過我們上面的知識(shí),我們知道每個(gè) Segment 又包含若干個(gè)散列表的桶,每個(gè)桶是由 HashEntry 鏈接起來的一個(gè)鏈表。如果 key 能夠均勻散列,每個(gè) Segment 大約守護(hù)整個(gè)散列表桶總數(shù)的 1/16。

下面我們還有通過一個(gè)圖來演示一下 ConcurrentHashMap 的結(jié)構(gòu):

43道多線程面試題,附帶答案(二)

并發(fā)寫操作

在 ConcurrentHashMap 中,當(dāng)執(zhí)行 put 方法的時(shí)候,會(huì)需要加鎖來完成。我們通過代碼來解釋一下具體過程: 當(dāng)我們 new 一個(gè) ConcurrentHashMap 對(duì)象,并且執(zhí)行put操作的時(shí)候,首先會(huì)執(zhí)行 ConcurrentHashMap 類中的 put 方法,該方法源碼為:

/**
?*?Maps?the?specified?key?to?the?specified?value?in?this?table.
?*?Neither?the?key?nor?the?value?can?be?null.
?*
?*?<p>?The?value?can?be?retrieved?by?calling?the?<tt>get</tt>?method
?*?with?a?key?that?is?equal?to?the?original?key.
?*
?*?@param?key?key?with?which?the?specified?value?is?to?be?associated
?*?@param?value?value?to?be?associated?with?the?specified?key
?*?@return?the?previous?value?associated?with?<tt>key</tt>,?or
?*?????????<tt>null</tt>?if?there?was?no?mapping?for?<tt>key</tt>
?*?@throws?NullPointerException?if?the?specified?key?or?value?is?null
?*/
@SuppressWarnings("unchecked")
public?V?put(K?key,?V?value)?{
????Segment<K,V>?s;
????if?(value?==?null)
????????throw?new?NullPointerException();
????int?hash?=?hash(key);
????int?j?=?(hash?>>>?segmentShift)?&?segmentMask;
????if?((s?=?(Segment<K,V>)UNSAFE.getObject??????????//?nonvolatile;?recheck
?????????(segments,?(j?<<?SSHIFT)?+?SBASE))?==?null)?//??in?ensureSegment
????????s?=?ensureSegment(j);
????return?s.put(key,?hash,?value,?false);
}

我們通過注釋可以了解到,ConcurrentHashMap 不允許空值。該方法首先有一個(gè) Segment 的引用 s,然后會(huì)通過 hash() 方法對(duì) key 進(jìn)行計(jì)算,得到哈希值;繼而通過調(diào)用 Segment 的 put(K key, int hash, V value, boolean onlyIfAbsent)方法進(jìn)行存儲(chǔ)操作。該方法源碼為:

final?V?put(K?key,?int?hash,?V?value,?boolean?onlyIfAbsent)?{
????//加鎖,這里是鎖定的Segment而不是整個(gè)ConcurrentHashMap
????HashEntry<K,V>?node?=?tryLock()???null?:scanAndLockForPut(key,?hash,?value);
????V?oldValue;
????try?{
????????HashEntry<K,V>[]?tab?=?table;
????????//得到hash對(duì)應(yīng)的table中的索引index
????????int?index?=?(tab.length?-?1)?&?hash;
????????//找到hash對(duì)應(yīng)的是具體的哪個(gè)桶,也就是哪個(gè)HashEntry鏈表
????????HashEntry<K,V>?first?=?entryAt(tab,?index);
????????for?(HashEntry<K,V>?e?=?first;;)?{
????????????if?(e?!=?null)?{
????????????????K?k;
????????????????if?((k?=?e.key)?==?key?||
????????????????????(e.hash?==?hash?&&?key.equals(k)))?{
????????????????????oldValue?=?e.value;
????????????????????if?(!onlyIfAbsent)?{
????????????????????????e.value?=?value;
????????????????????????++modCount;
????????????????????}
????????????????????break;
????????????????}
????????????????e?=?e.next;
????????????}
????????????else?{
????????????????if?(node?!=?null)
????????????????????node.setNext(first);
????????????????else
????????????????????node?=?new?HashEntry<K,V>(hash,?key,?value,?first);
????????????????int?c?=?count?+?1;
????????????????if?(c?>?threshold?&&?tab.length?<?MAXIMUM_CAPACITY)
????????????????????rehash(node);
????????????????else
????????????????????setEntryAt(tab,?index,?node);
????????????????++modCount;
????????????????count?=?c;
????????????????oldValue?=?null;
????????????????break;
????????????}
????????}
????}?finally?{
????????//解鎖
????????unlock();
????}
????return?oldValue;
}

關(guān)于該方法的某些關(guān)鍵步驟,在源碼上加上了注釋。

需要注意的是:加鎖操作是針對(duì)的 hash 值對(duì)應(yīng)的某個(gè) Segment,而不是整個(gè) ConcurrentHashMap。因?yàn)?put 操作只是在這個(gè) Segment 中完成,所以并不需要對(duì)整個(gè) ConcurrentHashMap 加鎖。所以,此時(shí),其他的線程也可以對(duì)另外的 Segment 進(jìn)行 put 操作,因?yàn)殡m然該 Segment 被鎖住了,但其他的 Segment 并沒有加鎖。同時(shí),讀線程并不會(huì)因?yàn)楸揪€程的加鎖而阻塞。

正是因?yàn)槠鋬?nèi)部的結(jié)構(gòu)以及機(jī)制,所以 ConcurrentHashMap 在并發(fā)訪問的性能上要比Hashtable和同步包裝之后的HashMap的性能提高很多。在理想狀態(tài)下,ConcurrentHashMap 可以支持 16 個(gè)線程執(zhí)行并發(fā)寫操作(如果并發(fā)級(jí)別設(shè)置為 16),及任意數(shù)量線程的讀操作。

總結(jié)

在實(shí)際的應(yīng)用中,散列表一般的應(yīng)用場(chǎng)景是:除了少數(shù)插入操作和刪除操作外,絕大多數(shù)都是讀取操作,而且讀操作在大多數(shù)時(shí)候都是成功的。正是基于這個(gè)前提,ConcurrentHashMap 針對(duì)讀操作做了大量的優(yōu)化。通過 HashEntry 對(duì)象的不變性和用 volatile 型變量協(xié)調(diào)線程間的內(nèi)存可見性,使得 大多數(shù)時(shí)候,讀操作不需要加鎖就可以正確獲得值。這個(gè)特性使得 ConcurrentHashMap 的并發(fā)性能在分離鎖的基礎(chǔ)上又有了近一步的提高。

ConcurrentHashMap 是一個(gè)并發(fā)散列映射表的實(shí)現(xiàn),它允許完全并發(fā)的讀取,并且支持給定數(shù)量的并發(fā)更新。相比于 HashTable 和用同步包裝器包裝的 HashMap(Collections.synchronizedMap(new HashMap())),ConcurrentHashMap 擁有更高的并發(fā)性。在 HashTable 和由同步包裝器包裝的 HashMap 中,使用一個(gè)全局的鎖來同步不同線程間的并發(fā)訪問。同一時(shí)間點(diǎn),只能有一個(gè)線程持有鎖,也就是說在同一時(shí)間點(diǎn),只能有一個(gè)線程能訪問容器。這雖然保證多線程間的安全并發(fā)訪問,但同時(shí)也導(dǎo)致對(duì)容器的訪問變成串行化的了。

ConcurrentHashMap 的高并發(fā)性主要來自于三個(gè)方面:

  • 用分離鎖實(shí)現(xiàn)多個(gè)線程間的更深層次的共享訪問。

  • 用 HashEntery 對(duì)象的不變性來降低執(zhí)行讀操作的線程在遍歷鏈表期間對(duì)加鎖的需求。

  • 通過對(duì)同一個(gè) Volatile 變量的寫 / 讀訪問,協(xié)調(diào)不同線程間讀 / 寫操作的內(nèi)存可見性。

使用分離鎖,減小了請(qǐng)求 同一個(gè)鎖的頻率。

通過 HashEntery 對(duì)象的不變性及對(duì)同一個(gè) Volatile 變量的讀 / 寫來協(xié)調(diào)內(nèi)存可見性,使得 讀操作大多數(shù)時(shí)候不需要加鎖就能成功獲取到需要的值。由于散列映射表在實(shí)際應(yīng)用中大多數(shù)操作都是成功的 讀操作,所以 2 和 3 既可以減少請(qǐng)求同一個(gè)鎖的頻率,也可以有效減少持有鎖的時(shí)間。通過減小請(qǐng)求同一個(gè)鎖的頻率和盡量減少持有鎖的時(shí)間 ,使得 ConcurrentHashMap 的并發(fā)性相對(duì)于 HashTable 和用同步包裝器包裝的 HashMap有了質(zhì)的提高。

39.BlockingQueue的使用?

BlockingQueue的原理

阻塞隊(duì)列(BlockingQueue)是一個(gè)支持兩個(gè)附加操作的隊(duì)列。這兩個(gè)附加的操作是:在隊(duì)列為空時(shí),獲取元素的線程會(huì)等待隊(duì)列變?yōu)榉强?。?dāng)隊(duì)列滿時(shí),存儲(chǔ)元素的線程會(huì)等待隊(duì)列可用。阻塞隊(duì)列常用于生產(chǎn)者和消費(fèi)者的場(chǎng)景,生產(chǎn)者是往隊(duì)列里添加元素的線程,消費(fèi)者是從隊(duì)列里拿元素的線程。阻塞隊(duì)列就是生產(chǎn)者存放元素的容器,而消費(fèi)者也只從容器里拿元素。

BlockingQueue的核心方法:

1)add(E e): 添加元素,如果BlockingQueue可以容納,則返回true,否則報(bào)異常

2)offer(E e): 添加元素,如果BlockingQueue可以容納,則返回true,否則返回false.

3)put(E e): 添加元素,如果BlockQueue沒有空間,則調(diào)用此方法的線程被阻斷直到BlockingQueue里面有空間再繼續(xù).

4)poll(long timeout, TimeUnit timeUnit): 取走BlockingQueue里排在首位的對(duì)象,若不能立即取出,則可以等timeout參數(shù)規(guī)定的時(shí)間,取不到時(shí)返回null

5)take(): 取走BlockingQueue里排在首位的對(duì)象,若BlockingQueue為空,阻斷進(jìn)入等待狀態(tài)直到Blocking有新的對(duì)象被加入為止

BlockingQueue常用實(shí)現(xiàn)類

1)ArrayBlockingQueue: 有界的先入先出順序隊(duì)列,構(gòu)造方法確定隊(duì)列的大小.

2)LinkedBlockingQueue: ×××的先入先出順序隊(duì)列,構(gòu)造方法提供兩種,一種初始化隊(duì)列大小,隊(duì)列即有界;第二種默認(rèn)構(gòu)造方法,隊(duì)列×××(有界即Integer.MAX_VALUE)

4)SynchronousQueue: 特殊的BlockingQueue,沒有空間的隊(duì)列,即必須有取的方法阻塞在這里的時(shí)候才能放入元素。

3)PriorityBlockingQueue: 支持優(yōu)先級(jí)的阻塞隊(duì)列 ,存入對(duì)象必須實(shí)現(xiàn)Comparator接口 (需要注意的是 隊(duì)列不是在加入元素的時(shí)候進(jìn)行排序,而是取出的時(shí)候,根據(jù)Comparator來決定優(yōu)先級(jí)最高的)。

BlockingQueue<> 隊(duì)列的作用

BlockingQueue 實(shí)現(xiàn)主要用于生產(chǎn)者-使用者隊(duì)列,BlockingQueue 實(shí)現(xiàn)是線程安全的。所有排隊(duì)方法都可以使用內(nèi)部鎖或其他形式的并發(fā)控制來自動(dòng)達(dá)到它們的目的

這是一個(gè)生產(chǎn)者-使用者場(chǎng)景的一個(gè)用例。注意,BlockingQueue 可以安全地與多個(gè)生產(chǎn)者和多個(gè)使用者一起使用 此用例來自jdk文檔

//這是一個(gè)生產(chǎn)者類
class?Producer?implements?Runnable?{
???private?final?BlockingQueue?queue;
???Producer(BlockingQueue?q)?{?
???????queue?=?q;?
???}
???public?void?run()?{
?????try?{
???????while(true)?{?
???????????queue.put(produce());?
???????}
?????}?catch?(InterruptedException?ex)?{?
?????????...?handle?...
?????????}
???}
???Object?produce()?{?
???????...?
???}
?}

?//這是一個(gè)消費(fèi)者類
?class?Consumer?implements?Runnable?{
???private?final?BlockingQueue?queue;
???Consumer(BlockingQueue?q)?{?queue?=?q;?}
???public?void?run()?{
?????try?{
???????while(true)?{?
???????????consume(queue.take());?
???????}
?????}?catch?(InterruptedException?ex)?{?
?????????...?handle?...
?????}
???}
???void?consume(Object?x)?{?
???????...?
???}
?}

?//這是實(shí)現(xiàn)類
?class?Setup?{
???void?main()?{
?????//實(shí)例一個(gè)非阻塞隊(duì)列
?????BlockingQueue?q?=?new?SomeQueueImplementation();
?????//將隊(duì)列傳入兩個(gè)消費(fèi)者和一個(gè)生產(chǎn)者中
?????Producer?p?=?new?Producer(q);
?????Consumer?c1?=?new?Consumer(q);
?????Consumer?c2?=?new?Consumer(q);
?????new?Thread(p).start();
?????new?Thread(c1).start();
?????new?Thread(c2).start();
???}
?}

40.ThreadPool的深入考察?

引言

合理利用線程池能夠帶來三個(gè)好處。第一:降低資源消耗。通過重復(fù)利用已創(chuàng)建的線程降低線程創(chuàng)建和銷毀造成的消耗。第二:提高響應(yīng)速度。當(dāng)任務(wù)到達(dá)時(shí),任務(wù)可以不需要等到線程創(chuàng)建就能立即執(zhí)行。第三:提高線程的可管理性。線程是稀缺資源,如果無限制的創(chuàng)建,不僅會(huì)消耗系統(tǒng)資源,還會(huì)降低系統(tǒng)的穩(wěn)定性,使用線程池可以進(jìn)行統(tǒng)一的分配,調(diào)優(yōu)和監(jiān)控。但是要做到合理的利用線程池,必須對(duì)其原理了如指掌。

線程池的使用

我們可以通過ThreadPoolExecutor來創(chuàng)建一個(gè)線程池。

new??ThreadPoolExecutor(corePoolSize,?maximumPoolSize,?keepAliveTime,?milliseconds,runnableTaskQueue,?handler);

創(chuàng)建一個(gè)線程池需要輸入幾個(gè)參數(shù):

  • corePoolSize(線程池的基本大?。寒?dāng)提交一個(gè)任務(wù)到線程池時(shí),線程池會(huì)創(chuàng)建一個(gè)線程來執(zhí)行任務(wù),即使其他空閑的基本線程能夠執(zhí)行新任務(wù)也會(huì)創(chuàng)建線程,等到需要執(zhí)行的任務(wù)數(shù)大于線程池基本大小時(shí)就不再創(chuàng)建。如果調(diào)用了線程池的prestartAllCoreThreads方法,線程池會(huì)提前創(chuàng)建并啟動(dòng)所有基本線程。

  • runnableTaskQueue(任務(wù)隊(duì)列):用于保存等待執(zhí)行的任務(wù)的阻塞隊(duì)列。 可以選擇以下幾個(gè)阻塞隊(duì)列。

    • ArrayBlockingQueue:是一個(gè)基于數(shù)組結(jié)構(gòu)的有界阻塞隊(duì)列,此隊(duì)列按 FIFO(先進(jìn)先出)原則對(duì)元素進(jìn)行排序。

    • LinkedBlockingQueue:一個(gè)基于鏈表結(jié)構(gòu)的阻塞隊(duì)列,此隊(duì)列按FIFO (先進(jìn)先出) 排序元素,吞吐量通常要高于ArrayBlockingQueue。靜態(tài)工廠方法Executors.newFixedThreadPool()使用了這個(gè)隊(duì)列。

    • SynchronousQueue:一個(gè)不存儲(chǔ)元素的阻塞隊(duì)列。每個(gè)插入操作必須等到另一個(gè)線程調(diào)用移除操作,否則插入操作一直處于阻塞狀態(tài),吞吐量通常要高于LinkedBlockingQueue,靜態(tài)工廠方法Executors.newCachedThreadPool使用了這個(gè)隊(duì)列。

    • PriorityBlockingQueue:一個(gè)具有優(yōu)先級(jí)的無限阻塞隊(duì)列。

  • maximumPoolSize(線程池最大大?。壕€程池允許創(chuàng)建的最大線程數(shù)。如果隊(duì)列滿了,并且已創(chuàng)建的線程數(shù)小于最大線程數(shù),則線程池會(huì)再創(chuàng)建新的線程執(zhí)行任務(wù)。值得注意的是如果使用了×××的任務(wù)隊(duì)列這個(gè)參數(shù)就沒什么效果。

  • ThreadFactory:用于設(shè)置創(chuàng)建線程的工廠,可以通過線程工廠給每個(gè)創(chuàng)建出來的線程設(shè)置更有意義的名字。

  • RejectedExecutionHandler(飽和策略):當(dāng)隊(duì)列和線程池都滿了,說明線程池處于飽和狀態(tài),那么必須采取一種策略處理提交的新任務(wù)。這個(gè)策略默認(rèn)情況下是AbortPolicy,表示無法處理新任務(wù)時(shí)拋出異常。以下是JDK1.5提供的四種策略。

    • AbortPolicy:直接拋出異常。

    • CallerRunsPolicy:只用調(diào)用者所在線程來運(yùn)行任務(wù)。

    • DiscardOldestPolicy:丟棄隊(duì)列里最近的一個(gè)任務(wù),并執(zhí)行當(dāng)前任務(wù)。

    • DiscardPolicy:不處理,丟棄掉。 當(dāng)然也可以根據(jù)應(yīng)用場(chǎng)景需要來實(shí)現(xiàn)RejectedExecutionHandler接口自定義策略。如記錄日志或持久化不能處理的任務(wù)。

  • keepAliveTime(線程活動(dòng)保持時(shí)間):線程池的工作線程空閑后,保持存活的時(shí)間。所以如果任務(wù)很多,并且每個(gè)任務(wù)執(zhí)行的時(shí)間比較短,可以調(diào)大這個(gè)時(shí)間,提高線程的利用率。

  • TimeUnit(線程活動(dòng)保持時(shí)間的單位):可選的單位有天(DAYS),小時(shí)(HOURS),分鐘(MINUTES),毫秒(MILLISECONDS),微秒(MICROSECONDS, 千分之一毫秒)和毫微秒(NANOSECONDS, 千分之一微秒)。

向線程池提交任務(wù)

我們可以使用execute提交的任務(wù),但是execute方法沒有返回值,所以無法判斷任務(wù)是否被線程池執(zhí)行成功。通過以下代碼可知execute方法輸入的任務(wù)是一個(gè)Runnable類的實(shí)例。

threadsPool.execute(new?Runnable()?{
????@Override
????public?void?run()?{
????????//?TODO?Auto-generated?method?stub
????}
});

我們也可以使用submit 方法來提交任務(wù),它會(huì)返回一個(gè)future,那么我們可以通過這個(gè)future來判斷任務(wù)是否執(zhí)行成功,通過future的get方法來獲取返回值,get方法會(huì)阻塞住直到任務(wù)完成,而使用get(long timeout, TimeUnit unit)方法則會(huì)阻塞一段時(shí)間后立即返回,這時(shí)有可能任務(wù)沒有執(zhí)行完。

Future<Object>?future?=?executor.submit(harReturnValuetask);try?{?????Object?s?=?future.get();
}?catch?(InterruptedException?e)?{????//?處理中斷異常}?catch?(ExecutionException?e)?{????//?處理無法執(zhí)行任務(wù)異常}?finally?{????//?關(guān)閉線程池
????executor.shutdown();
}

線程池的關(guān)閉

我們可以通過調(diào)用線程池的shutdown或shutdownNow方法來關(guān)閉線程池,它們的原理是遍歷線程池中的工作線程,然后逐個(gè)調(diào)用線程的interrupt方法來中斷線程,所以無法響應(yīng)中斷的任務(wù)可能永遠(yuǎn)無法終止。但是它們存在一定的區(qū)別,shutdownNow首先將線程池的狀態(tài)設(shè)置成STOP,然后嘗試停止所有的正在執(zhí)行或暫停任務(wù)的線程,并返回等待執(zhí)行任務(wù)的列表,而shutdown只是將線程池的狀態(tài)設(shè)置成SHUTDOWN狀態(tài),然后中斷所有沒有正在執(zhí)行任務(wù)的線程。

只要調(diào)用了這兩個(gè)關(guān)閉方法的其中一個(gè),isShutdown方法就會(huì)返回true。當(dāng)所有的任務(wù)都已關(guān)閉后,才表示線程池關(guān)閉成功,這時(shí)調(diào)用isTerminaed方法會(huì)返回true。至于我們應(yīng)該調(diào)用哪一種方法來關(guān)閉線程池,應(yīng)該由提交到線程池的任務(wù)特性決定,通常調(diào)用shutdown來關(guān)閉線程池,如果任務(wù)不一定要執(zhí)行完,則可以調(diào)用shutdownNow。

線程池的分析

流程分析:線程池的主要工作流程如下圖:

43道多線程面試題,附帶答案(二)

從上圖我們可以看出,當(dāng)提交一個(gè)新任務(wù)到線程池時(shí),線程池的處理流程如下:

  1. 首先線程池判斷基本線程池是否已滿?沒滿,創(chuàng)建一個(gè)工作線程來執(zhí)行任務(wù)。滿了,則進(jìn)入下個(gè)流程。

  2. 其次線程池判斷工作隊(duì)列是否已滿?沒滿,則將新提交的任務(wù)存儲(chǔ)在工作隊(duì)列里。滿了,則進(jìn)入下個(gè)流程。

  3. 最后線程池判斷整個(gè)線程池是否已滿?沒滿,則創(chuàng)建一個(gè)新的工作線程來執(zhí)行任務(wù),滿了,則交給飽和策略來處理這個(gè)任務(wù)。

源碼分析

上面的流程分析讓我們很直觀的了解了線程池的工作原理,讓我們?cè)偻ㄟ^源代碼來看看是如何實(shí)現(xiàn)的。線程池執(zhí)行任務(wù)的方法如下:

public?void?execute(Runnable?command)?{????if?(command?==?null)???????throw?new?NullPointerException();????//如果線程數(shù)小于基本線程數(shù),則創(chuàng)建線程并執(zhí)行當(dāng)前任務(wù)?
????if?(poolSize?>=?corePoolSize?||?!addIfUnderCorePoolSize(command))?{????//如線程數(shù)大于等于基本線程數(shù)或線程創(chuàng)建失敗,則將當(dāng)前任務(wù)放到工作隊(duì)列中。
????????if?(runState?==?RUNNING?&&?workQueue.offer(command))?{????????????if?(runState?!=?RUNNING?||?poolSize?==?0)
??????????????????????ensureQueuedTaskHandled(command);
????????}????//如果線程池不處于運(yùn)行中或任務(wù)無法放入隊(duì)列,并且當(dāng)前線程數(shù)量小于最大允許的線程數(shù)量,則創(chuàng)建一個(gè)線程執(zhí)行任務(wù)。????????else?if?(!addIfUnderMaximumPoolSize(command))????????//拋出RejectedExecutionException異常
????????????reject(command);?//?is?shutdown?or?saturated
????}
}

工作線程。線程池創(chuàng)建線程時(shí),會(huì)將線程封裝成工作線程Worker,Worker在執(zhí)行完任務(wù)后,還會(huì)無限循環(huán)獲取工作隊(duì)列里的任務(wù)來執(zhí)行。我們可以從Worker的run方法里看到這點(diǎn):

public?void?run()?{?????try?{
???????????Runnable?task?=?firstTask;
???????????firstTask?=?null;????????????while?(task?!=?null?||?(task?=?getTask())?!=?null)?{
????????????????????runTask(task);
????????????????????task?=?null;
????????????}
??????}?finally?{
?????????????workerDone(this);
??????}
}

合理的配置線程池

要想合理的配置線程池,就必須首先分析任務(wù)特性,可以從以下幾個(gè)角度來進(jìn)行分析:

  1. 任務(wù)的性質(zhì):CPU密集型任務(wù),IO密集型任務(wù)和混合型任務(wù)。

  2. 任務(wù)的優(yōu)先級(jí):高,中和低。

  3. 任務(wù)的執(zhí)行時(shí)間:長,中和短。

  4. 任務(wù)的依賴性:是否依賴其他系統(tǒng)資源,如數(shù)據(jù)庫連接。

任務(wù)性質(zhì)不同的任務(wù)可以用不同規(guī)模的線程池分開處理。CPU密集型任務(wù)配置盡可能小的線程,如配置Ncpu+1個(gè)線程的線程池。IO密集型任務(wù)則由于線程并不是一直在執(zhí)行任務(wù),則配置盡可能多的線程,如2*Ncpu。混合型的任務(wù),如果可以拆分,則將其拆分成一個(gè)CPU密集型任務(wù)和一個(gè)IO密集型任務(wù),只要這兩個(gè)任務(wù)執(zhí)行的時(shí)間相差不是太大,那么分解后執(zhí)行的吞吐率要高于串行執(zhí)行的吞吐率,如果這兩個(gè)任務(wù)執(zhí)行時(shí)間相差太大,則沒必要進(jìn)行分解。我們可以通過Runtime.getRuntime().availableProcessors()方法獲得當(dāng)前設(shè)備的CPU個(gè)數(shù)。

優(yōu)先級(jí)不同的任務(wù)可以使用優(yōu)先級(jí)隊(duì)列PriorityBlockingQueue來處理。它可以讓優(yōu)先級(jí)高的任務(wù)先得到執(zhí)行,需要注意的是如果一直有優(yōu)先級(jí)高的任務(wù)提交到隊(duì)列里,那么優(yōu)先級(jí)低的任務(wù)可能永遠(yuǎn)不能執(zhí)行。

執(zhí)行時(shí)間不同的任務(wù)可以交給不同規(guī)模的線程池來處理,或者也可以使用優(yōu)先級(jí)隊(duì)列,讓執(zhí)行時(shí)間短的任務(wù)先執(zhí)行。

依賴數(shù)據(jù)庫連接池的任務(wù),因?yàn)榫€程提交SQL后需要等待數(shù)據(jù)庫返回結(jié)果,如果等待的時(shí)間越長CPU空閑時(shí)間就越長,那么線程數(shù)應(yīng)該設(shè)置越大,這樣才能更好的利用CPU。

建議使用有界隊(duì)列,有界隊(duì)列能增加系統(tǒng)的穩(wěn)定性和預(yù)警能力,可以根據(jù)需要設(shè)大一點(diǎn),比如幾千。有一次我們組使用的后臺(tái)任務(wù)線程池的隊(duì)列和線程池全滿了,不斷的拋出拋棄任務(wù)的異常,通過排查發(fā)現(xiàn)是數(shù)據(jù)庫出現(xiàn)了問題,導(dǎo)致執(zhí)行SQL變得非常緩慢,因?yàn)楹笈_(tái)任務(wù)線程池里的任務(wù)全是需要向數(shù)據(jù)庫查詢和插入數(shù)據(jù)的,所以導(dǎo)致線程池里的工作線程全部阻塞住,任務(wù)積壓在線程池里。如果當(dāng)時(shí)我們?cè)O(shè)置成×××隊(duì)列,線程池的隊(duì)列就會(huì)越來越多,有可能會(huì)撐滿內(nèi)存,導(dǎo)致整個(gè)系統(tǒng)不可用,而不只是后臺(tái)任務(wù)出現(xiàn)問題。當(dāng)然我們的系統(tǒng)所有的任務(wù)是用的單獨(dú)的服務(wù)器部署的,而我們使用不同規(guī)模的線程池跑不同類型的任務(wù),但是出現(xiàn)這樣問題時(shí)也會(huì)影響到其他任務(wù)。

線程池的監(jiān)控

通過線程池提供的參數(shù)進(jìn)行監(jiān)控。線程池里有一些屬性在監(jiān)控線程池的時(shí)候可以使用

  • taskCount:線程池需要執(zhí)行的任務(wù)數(shù)量。

  • completedTaskCount:線程池在運(yùn)行過程中已完成的任務(wù)數(shù)量。小于或等于taskCount。

  • largestPoolSize:線程池曾經(jīng)創(chuàng)建過的最大線程數(shù)量。通過這個(gè)數(shù)據(jù)可以知道線程池是否滿過。如等于線程池的最大大小,則表示線程池曾經(jīng)滿了。

  • getPoolSize:線程池的線程數(shù)量。如果線程池不銷毀的話,池里的線程不會(huì)自動(dòng)銷毀,所以這個(gè)大小只增不+getActiveCount:獲取活動(dòng)的線程數(shù)。

通過擴(kuò)展線程池進(jìn)行監(jiān)控。通過繼承線程池并重寫線程池的beforeExecute,afterExecute和terminated方法,我們可以在任務(wù)執(zhí)行前,執(zhí)行后和線程池關(guān)閉前干一些事情。如監(jiān)控任務(wù)的平均執(zhí)行時(shí)間,最大執(zhí)行時(shí)間和最小執(zhí)行時(shí)間等。這幾個(gè)方法在線程池里是空方法。如:

protected?void?beforeExecute(Thread?t,?Runnable?r)?{?}

4

網(wǎng)頁名稱:43道多線程面試題,附帶答案(二)
URL標(biāo)題:http://bm7419.com/article10/jdihdo.html

成都網(wǎng)站建設(shè)公司_創(chuàng)新互聯(lián),為您提供企業(yè)網(wǎng)站制作、小程序開發(fā)、軟件開發(fā)、網(wǎng)站設(shè)計(jì)App開發(fā)、手機(jī)網(wǎng)站建設(shè)

廣告

聲明:本網(wǎng)站發(fā)布的內(nèi)容(圖片、視頻和文字)以用戶投稿、用戶轉(zhuǎn)載內(nèi)容為主,如果涉及侵權(quán)請(qǐng)盡快告知,我們將會(huì)在第一時(shí)間刪除。文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如需處理請(qǐng)聯(lián)系客服。電話:028-86922220;郵箱:631063699@qq.com。內(nèi)容未經(jīng)允許不得轉(zhuǎn)載,或轉(zhuǎn)載時(shí)需注明來源: 創(chuàng)新互聯(lián)

成都定制網(wǎng)站網(wǎng)頁設(shè)計(jì)