吐血總結 - Java 併發編程 72 變
大家好,面試連環炮系列,繼續走起,今天給大家分享的 Java 併發編程面試連環炮。我寫公衆號的宗旨是:希望能給你帶來技術上或認識上有所幫助,真心的希望你有所收穫
。
希望通過這種連環炮的方式,讓大家更好吸收知識點,同時也是面試中出現頻率非常高。
上帝視角
廢話不多說,直奔主題。
開始發炮
1、 什麼是線程?
線程是操作系統能夠進行運算調度的最小單位,它被包含在進程之中,是進程中的實際運作單位。程序員可以通過它進行多處理器編程,你可以使用多線程對運算密集型任務提速。比如,如果一個線程完成一個任務要100毫秒
,那麼用十個線程完成該任務只需10毫秒
。
2、 線程和進程有什麼區別?
一個進程是一個獨立 (self contained
) 的運行環境,它可以被看作一個程序或者一個應用。而線程是在進程中執行的一個任務。線程是進程的子集,一個進程可以有很多線程,每條線程並行執行不同的任務。不同的進程使用不同的內存空間,而所有的線程共享一片相同的內存空間。別把它和棧內存搞混,每個線程都擁有單獨的棧內存用來存儲本地數據。
3、 如何在 Java 中實現線程?
有兩種創建線程的方法:一是實現Runnable
接口,然後將它傳遞給Thread
的構造函數,創建一個Thread
對象;二是直接繼承Thread
類。
4、 用 Runnable 還是 Thread?
這個問題是上題的後續,大家都知道我們可以通過繼承 Thread 類或者調用Runnable
接口來實現線程,問題是,那個方法更好呢?什麼情況下使用它?
這個問題很容易回答,如果你知道Java
不支持類的多重繼承,但允許你調用多個接口。所以如果你要繼承其他類,當然是調用Runnable
接口好了。更多詳細信息請點擊這裏。
5、 Thread 類中的 start() 和 run() 方法有什麼區別?
start()
方法被用來啓動新創建的線程,使該被創建的線程狀態變爲可運行狀態。當你調用run()
方法的時候,只會是在原來的線程中調用,沒有新的線程啓動,start()
方法纔會啓動新線程。如果我們調用了Thread
的run()
方法,它的行爲就會和普通的方法一樣,直接運行run()
方法。爲了在新的線程中執行我們的代碼,必須使用Thread.start()
方法。
6、 Java 中 Runnable 和 Callable 有什麼不同?
Runnable
和Callable
都代表那些要在不同的線程中執行的任務。Runnable 從 JDK1.0 開始就有了,Callable 是在 JDK1.5 增加的。它們的主要區別是 Callable 的 call() 方法可以返回值和拋出異常,而 Runnable 的 run() 方法沒有這些功能。Callable 可以返回裝載有計算結果的 Future 對象。
7、 Java 中 CyclicBarrier 和 CountDownLatch 有什麼不同?
CyclicBarrier
和 CountDownLatch
都可以用來讓一組線程等待其它線程。與 CyclicBarrier
不同的是,CountdownLatch
不能重新使用。
8、 Java 內存模型是什麼?
Java 內存模型規定和指引 Java 程序在不同的內存架構、CPU 和操作系統間有確定性地行爲。它在多線程的情況下尤其重要。Java 內存模型對一個線程所做的變動能被其它線程可見提供了保證,它們之間是先行發生關係。這個關係定義了一些規則讓程序員在併發編程時思路更清晰。比如,先行發生關係確保了:
-
線程內的代碼能夠按先後順序執行,這被稱爲程序次序規則。
-
對於同一個鎖,一個解鎖操作一定要發生在時間上後發生的另一個鎖定操作之前,也叫做管程鎖定規則。
-
前一個對
volatile
的寫操作在後一個volatile
的讀操作之前,也叫volatile
變量規則。 -
一個線程內的任何操作必需在這個線程的
start()
調用之後,也叫作線程啓動規則。 -
一個線程的所有操作都會在線程終止之前,線程終止規則。
-
一個對象的終結操作必需在這個對象構造完成之後,也叫對象終結規則。
-
可傳遞性
強烈建議大家閱讀《Java 併發編程實踐》第十六章來加深對 Java 內存模型的理解。
9、 Java 中的 volatile 變量是什麼?
volatile
是一個特殊的修飾符,只有成員變量才能使用它。在Java
併發程序缺少同步類的情況下,多線程對成員變量的操作對其它線程是透明的。volatile
變量可以保證下一個讀取操作會在前一個寫操作之後發生。線程都會直接從內存中讀取該變量並且不緩存它。這就確保了線程讀取到的變量是同內存中是一致的。
10、 什麼是線程安全?Vector 是一個線程安全類嗎?
如果你的代碼所在的進程中有多個線程在同時運行,而這些線程可能會同時運行這段代碼。如果每次運行結果和單線程運行的結果是一樣的,而且其他的變量的值也和預期的是一樣的,就是線程安全的。一個線程安全的計數器類的同一個實例對象在被多個線程使用的情況下也不會出現計算失誤。很顯然你可以將集合類分成兩組,線程安全和非線程安全的。Vector 是用同步方法來實現線程安全的, 而和它相似的ArrayList
不是線程安全的。
11、 Java 中什麼是競態條件?
在大多數實際的多線程應用中,兩個或兩個以上的線程需要共享對同一數據的存取。如果 i 線程存取相同的對象,並且每一個線程都調用了一個修改該對象狀態的方法,將會發生什麼呢?可以想象,線程彼此踩了對方的腳。根據線程訪問數據的次序,可能會產生訛誤的對象。這樣的情況通常稱爲競爭條件。
12、 Java 中如何停止一個線程?
Java
提供了很豐富的 API 但沒有爲停止線程提供 API。JDK 1.0 本來有一些像stop()
, suspend()
和 resume()
的控制方法,但是由於潛在的死鎖威脅。因此在後續的 JDK 版本中他們被棄用了,之後Java API
的設計者就沒有提供一個兼容且線程安全的方法來停止一個線程。當run()
或者 call()
方法執行完的時候線程會自動結束,如果要手動結束一個線程,可以用volatile
布爾變量來退出run()
方法的循環或者是取消任務來中斷線程。
13、 一個線程運行時發生異常會怎樣?
如果異常沒有被捕獲該線程將會停止執行。Thread.UncaughtExceptionHandler
是用於處理未捕獲異常造成線程突然中斷情況的一個內嵌接口。當一個未捕獲異常將造成線程中斷的時候 JVM 會使用Thread.getUncaughtExceptionHandler()
來查詢線程的UncaughtExceptionHandler
並將線程和異常作爲參數傳遞給handler
的uncaughtException()
方法進行處理。
14、如何在兩個線程間共享數據?
你可以通過共享對象來實現這個目的,或者是使用像阻塞隊列這樣併發的數據結構。這篇教程《Java 線程間通信》(涉及到在兩個線程間共享對象) 用 wait 和 notify 方法實現了生產者消費者模型。
15、 Java 中 notify 和 notifyAll 有什麼區別?
這又是一個刁鑽的問題,因爲多線程可以等待單監控鎖,Java API
的設計人員提供了一些方法當等待條件改變的時候通知它們,但是這些方法沒有完全實現。notify() 方法不能喚醒某個具體的線程,所以只有一個線程在等待的時候它纔有用武之地。而notifyAll()
喚醒所有線程並允許他們爭奪鎖確保了至少有一個線程能繼續運行。
16、爲什麼 wait, notify 和 notifyAll 這些方法不在 thread 類裏面?
一個很明顯的原因是 JAVA 提供的鎖是對象級的而不是線程級的,每個對象都有鎖,通過線程獲得。如果線程需要等待某些鎖那麼調用對象中的wait()
方法就有意義了。如果wait()
方法定義在Thread
類中,線程正在等待的是哪個鎖就不明顯了。簡單的說,由於 wait,notify
和notifyAll
都是鎖級別的操作,所以把他們定義在Object
類中因爲鎖屬於對象。
17、 什麼是 ThreadLocal 變量?
ThreadLocal
是 Java 裏一種特殊的變量。每個線程都有一個ThreadLocal
就是每個線程都擁有了自己獨立的一個變量,競爭條件被徹底消除了。如果爲每個線程提供一個自己獨有的變量拷貝,將大大提高效率。首先,通過複用減少了代價高昂的對象的創建個數。其次,你在沒有使用高代價的同步或者不變性的情況下獲得了線程安全。
18、 什麼是 FutureTask?
在 Java 併發程序中FutureTask
表示一個可以取消的異步運算。它有啓動和取消運算、查詢運算是否完成和取回運算結果等方法。只有當運算完成的時候結果才能取回,如果運算尚未完成 get 方法將會阻塞。一個FutureTask
對象可以對調用了Callable
和Runnable
的對象進行包裝,由於FutureTask
也是調用了Runnable
接口所以它可以提交給 Executor 來執行。
19、 Java 中 interrupted 和 isInterruptedd 方法的區別?
interrupted()
和isInterrupted()
的主要區別是前者會將中斷狀態清除而後者不會。Java 多線程的中斷機制是用內部標識來實現的,調用Thread.interrupt()
來中斷一個線程就會設置中斷標識爲 true。當中斷線程調用靜態方法Thread.interrupted()
來檢查中斷狀態時,中斷狀態會被清零。而非靜態方法isInterrupted()
用來查詢其它線程的中斷狀態且不會改變中斷狀態標識。簡單的說就是任何拋出InterruptedException
異常的方法都會將中斷狀態清零。無論如何,一個線程的中斷狀態有有可能被其它線程調用中斷來改變。
20、爲什麼 wait 和 notify 方法要在同步塊中調用?
當一個線程需要調用對象的wait()
方法的時候,這個線程必須擁有該對象的鎖,接着它就會釋放這個對象鎖並進入等待狀態直到其他線程調用這個對象上的notify()
方法。同樣的,當一個線程需要調用對象的notify()
方法時,它會釋放這個對象的鎖,以便其他在等待的線程就可以得到這個對象鎖。由於所有的這些方法都需要線程持有對象的鎖,這樣就只能通過同步來實現,所以他們只能在同步方法或者同步塊中被調用。如果你不這麼做,代碼會拋出IllegalMonitorStateException
異常。
21、 爲什麼你應該在循環中檢查等待條件?
處於等待狀態的線程可能會收到錯誤警報和僞喚醒,如果不在循環中檢查等待條件,程序就會在沒有滿足結束條件的情況下退出。因此,當一個等待線程醒來時,不能認爲它原來的等待狀態仍然是有效的,在notify()
方法調用之後和等待線程醒來之前這段時間它可能會改變。這就是在循環中使用wait()
方法效果更好的原因,你可以在 Eclipse 中創建模板調用wait
和notify
試一試。如果你想了解更多關於這個問題的內容,推薦你閱讀《Effective Java》這本書中的線程和同步章節。
22、 Java 中的同步集合與併發集合有什麼區別?
同步集合與併發集合都爲多線程和併發提供了合適的線程安全的集合,不過併發集合的可擴展性更高。在 Java1.5 之前程序員們只有同步集合來用且在多線程併發的時候會導致爭用,阻礙了系統的擴展性。Java5 介紹了併發集合像ConcurrentHashMap
,不僅提供線程安全還用鎖分離和內部分區等現代技術提高了可擴展性。
23、 Java 中堆和棧有什麼不同?
爲什麼把這個問題歸類在多線程和併發面試題裏?因爲棧是一塊和線程緊密相關的內存區域。每個線程都有自己的棧內存,用於存儲本地變量,方法參數和棧調用,一個線程中存儲的變量對其它線程是不可見的。而堆是所有線程共享的一片公用內存區域。對象都在堆裏創建,爲了提升效率線程會從堆中弄一個緩存到自己的棧,如果多個線程使用該變量就可能引發問題,這時volatile
變量就可以發揮作用了,它要求線程從主存中讀取變量的值。
24、 什麼是線程池?爲什麼要使用它?
創建線程要花費昂貴的資源和時間,如果任務來了才創建線程那麼響應時間會變長,而且一個進程能創建的線程數有限。爲了避免這些問題,在程序啓動的時候就創建若干線程來響應處理,它們被稱爲線程池,裏面的線程叫工作線程。從JDK1.5
開始,Java API
提供了Executor
框架讓你可以創建不同的線程池。比如單線程池,每次處理一個任務;數目固定的線程池或者是緩存線程池(一個適合很多生存期短的任務的程序的可擴展線程池)。
25、 如何寫代碼來解決生產者消費者問題?
在現實中你解決的許多線程問題都屬於生產者消費者模型,就是一個線程生產任務供其它線程進行消費,你必須知道怎麼進行線程間通信來解決這個問題。比較低級的辦法是用 wait 和 notify 來解決這個問題,比較讚的辦法是用 Semaphore 或者 BlockingQueue
來實現生產者消費者模型。
26、 如何避免死鎖?
Java 多線程中的死鎖
死鎖是指兩個或兩個以上的進程在執行過程中,因爭奪資源而造成的一種互相等待的現象,若無外力作用,它們都將無法推進下去。這是一個嚴重的問題,因爲死鎖會讓你的程序掛起無法完成任務,死鎖的發生必須滿足以下四個條件:
-
互斥條件:一個資源每次只能被一個進程使用。
-
請求與保持條件:一個進程因請求資源而阻塞時,對已獲得的資源保持不放。
-
不剝奪條件:進程已獲得的資源,在末使用完之前,不能強行剝奪。
-
循環等待條件:若干進程之間形成一種頭尾相接的循環等待資源關係。
避免死鎖最簡單的方法就是阻止循環等待條件,將系統中所有的資源設置標誌位、排序,規定所有的進程申請資源必須以一定的順序(升序或降序)做操作來避免死鎖。
27、 Java 中活鎖和死鎖有什麼區別?
這是上題的擴展,活鎖和死鎖類似,不同之處在於處於活鎖的線程或進程的狀態是不斷改變的,活鎖可以認爲是一種特殊的飢餓。一個現實的活鎖例子是兩個人在狹小的走廊碰到,兩個人都試着避讓對方好讓彼此通過,但是因爲避讓的方向都一樣導致最後誰都不能通過走廊。簡單的說就是,活鎖和死鎖的主要區別是前者進程的狀態可以改變但是卻不能繼續執行。
28、 怎麼檢測一個線程是否擁有鎖?
在java.lang.Thread
中有一個方法叫holdsLock()
,它返回 true 如果當且僅當當前線程擁有某個具體對象的鎖。
29、 你如何在 Java 中獲取線程堆棧?
對於不同的操作系統,有多種方法來獲得 Java 進程的線程堆棧。當你獲取線程堆棧時,JVM 會把所有線程的狀態存到日誌文件或者輸出到控制檯。在 Windows 你可以使用Ctrl + Break
組合鍵來獲取線程堆棧,Linux
下用kill -3
命令。你也可以用jstack
這個工具來獲取,它對線程 id 進行操作,你可以用jps
這個工具找到 id。
30、 JVM 中哪個參數是用來控制線程的棧堆棧小的
這個問題很簡單,-Xss
參數用來控制線程的堆棧大小。你可以查看JVM
配置列表來了解這個參數的更多信息。
31、 Java 中 synchronized 和 ReentrantLock 有什麼不同?
Java
在過去很長一段時間只能通過synchronized
關鍵字來實現互斥,它有一些缺點。比如你不能擴展鎖之外的方法或者塊邊界,嘗試獲取鎖時不能中途取消等。Java 5 通過Lock
接口提供了更復雜的控制來解決這些問題。ReentrantLock
類實現了 Lock,它擁有與 synchronized
相同的併發性和內存語義且它還具有可擴展性。
32、 有三個線程 T1,T2,T3,怎麼確保它們按順序執行(確保 main() 方法所在的線程是 Java 程序最後結束的線程)?
在多線程中有多種方法讓線程按特定順序執行,你可以用線程類的join()
方法在一個線程中啓動另一個線程,另外一個線程完成該線程繼續執行。爲了確保三個線程的順序你應該先啓動最後一個 (T3 調用 T2,T2 調用 T1),這樣 T1 就會先完成而 T3 最後完成。
33、 Thread 類中的 yield 方法有什麼作用?
yield 方法可以暫停當前正在執行的線程對象,讓其它有相同優先級的線程執行。它是一個靜態方法而且只保證當前線程放棄CPU
佔用而不能保證使其它線程一定能佔用CPU
,執行yield()
的線程有可能在進入到暫停狀態後馬上又被執行。點擊這裏查看更多yield
方法的相關內容。
34、 Java 中 ConcurrentHashMap 的併發度是什麼?
ConcurrentHashMap
把實際 map 劃分成若干部分來實現它的可擴展性和線程安全。這種劃分是使用併發度獲得的,它是ConcurrentHashMap
類構造函數的一個可選參數,默認值爲 16,這樣在多線程情況下就能避免爭用。
35、Java 中 Semaphore 是什麼?
Java 中的 Semaphore 是一種新的同步類,它是一個計數信號。從概念上講,從概念上講,信號量維護了一個許可集合。如有必要,在許可可用前會阻塞每一個 acquire()
,然後再獲取該許可。每個 release()
添加一個許可,從而可能釋放一個正在阻塞的獲取者。但是,不使用實際的許可對象,Semaphore
只對可用許可的號碼進行計數,並採取相應的行動。信號量常常用於多線程的代碼中,比如數據庫連接池。更多詳細信息請點擊這裏。
36、如果你提交任務時,線程池隊列已滿。會時發會生什麼?
這個問題問得很狡猾,許多程序員會認爲該任務會阻塞直到線程池隊列有空位。事實上如果一個任務不能被調度執行那麼ThreadPoolExecutor’s submit()
方法將會拋出一個RejectedExecutionException
異常。
37、 Java 線程池中 submit() 和 execute() 方法有什麼區別?
兩個方法都可以向線程池提交任務,execute()
方法的返回類型是void
,它定義在Executor
接口中, 而submit()
方法可以返回持有計算結果的 Future 對象,它定義在ExecutorService
接口中,它擴展了Executor
接口,其它線程池類像ThreadPoolExecutor
和ScheduledThreadPoolExecutor
都有這些方法。更多詳細信息請點擊這裏。
38、 什麼是阻塞式方法?
阻塞式方法是指程序會一直等待該方法完成期間不做其他事情,ServerSocket
的accept()方
法就是一直等待客戶端連接。這裏的阻塞是指調用結果返回之前,當前線程會被掛起,直到得到結果之後纔會返回。此外,還有異步和非阻塞式方法在任務完成前就返回。更多詳細信息請點擊這裏。
39、 你對線程優先級的理解是什麼?
每一個線程都是有優先級的,一般來說,高優先級的線程在運行時會具有優先權,但這依賴於線程調度的實現,這個實現是和操作系統相關的 (OS dependent
)。我們可以定義線程的優先級,但是這並不能保證高優先級的線程會在低優先級的線程前執行。線程優先級是一個int變量(從1-10)
,1 代表最低優先級,10 代表最高優先級。
40、 什麼是線程調度器 (Thread Scheduler) 和時間分片(Time Slicing)?
線程調度器是一個操作系統服務,它負責爲Runnable
狀態的線程分配 CPU 時間。一旦我們創建一個線程並啓動它,它的執行便依賴於線程調度器的實現。時間分片是指將可用的 CPU 時間分配給可用的Runnable
線程的過程。分配 CPU 時間可以基於線程優先級或者線程等待的時間。線程調度並不受到 Java 虛擬機控制,所以由應用程序來控制它是更好的選擇(也就是說不要讓你的程序依賴於線程的優先級
)。
41、 在多線程中,什麼是上下文切換 (context-switching)?
上下文切換
是存儲和恢復 CPU 狀態的過程,它使得線程執行能夠從中斷點恢復執行。上下文切換
是多任務操作系統和多線程環境的基本特徵。
42、 如何在 Java 中創建 Immutable 對象?
Immutable
對象可以在沒有同步的情況下共享,降低了對該對象進行併發訪問時的同步化開銷。要創建不可變類,要實現下面幾個步驟:通過構造方法初始化所有成員、對變量不要提供setter
方法、將所有的成員聲明爲私有的,這樣就不允許直接訪問這些成員、在getter
方法中,不要直接返回對象本身,而是克隆對象,並返回對象的拷貝。
43、 Java 中的 ReadWriteLock 是什麼?
一般而言,讀寫鎖是用來提升併發程序性能的鎖分離技術的成果。Java 中的ReadWriteLock
是 Java 5 中新增的一個接口,一個ReadWriteLock
維護一對關聯的鎖,一個用於只讀操作一個用於寫。在沒有寫線程的情況下一個讀鎖可能會同時被多個讀線程持有。寫鎖是獨佔的,你可以使用JDK
中的ReentrantReadWriteLock
來實現這個規則,它最多支持 65535 個寫鎖和 65535 個讀鎖。
44、 多線程中的忙循環是什麼?
忙循環就是程序員用循環讓一個線程等待,不像傳統方法wait()
, sleep()
或yield()
它們都放棄了 CPU 控制,而忙循環不會放棄 CPU,它就是在運行一個空循環。這麼做的目的是爲了保留 CPU 緩存,在多核系統中,一個等待線程醒來的時候可能會在另一個內核運行,這樣會重建緩存。爲了避免重建緩存和減少等待重建的時間就可以使用它了。
45、volatile 變量和 atomic 變量有什麼不同?
這是個有趣的問題。首先,volatile
變量和 atomic
變量看起來很像,但功能卻不一樣。Volatile
變量可以確保先行關係,即寫操作會發生在後續的讀操作之前, 但它並不能保證原子性。例如用 volatile 修飾count
變量那麼 count++
操作就不是原子性的。而AtomicInteger
類提供的atomic
方法可以讓這種操作具有原子性如getAndIncrement()
方法會原子性的進行增量操作把當前值加一,其它數據類型和引用變量也可以進行相似操作。
46、 如果同步塊內的線程拋出異常會發生什麼?
這個問題坑了很多Java
程序員,若你能想到鎖是否釋放這條線索來回答還有點希望答對。無論你的同步塊是正常還是異常退出的,裏面的線程都會釋放鎖,所以對比鎖接口我們更喜歡同步塊,因爲它不用花費精力去釋放鎖,該功能可以在finally block
裏釋放鎖實現。
47、 單例模式的雙檢鎖是什麼?
這個問題在Java
面試中經常被問到,但是面試官對回答此問題的滿意度僅爲50%
。一半的人寫不出雙檢鎖還有一半的人說不出它的隱患和Java1.5
是如何對它修正的。它其實是一個用來創建線程安全的單例的老方法,當單例實例第一次被創建時它試圖用單個鎖進行性能優化,但是由於太過於複雜在JDK1.4
中它是失敗的。
48、 如何在 Java 中創建線程安全的 Singleton?
這是上面那個問題的後續,如果你不喜歡雙檢鎖而面試官問了創建Singleton
類的替代方法,你可以利用JVM
的類加載和靜態變量初始化特徵來創建Singleton
實例,或者是利用枚舉類型來創建Singleton
。
49、 寫出 3 條你遵循的多線程最佳實踐
以下三條最佳實踐大多數 Java 程序員都應該遵循:
- 給你的線程起個有意義的名字。
這樣可以方便找 bug 或追蹤。OrderProcessor
,QuoteProcessor or TradeProcessor
這種名字比 Thread-1.Thread-2 and Thread-3
好多了,給線程起一個和它要完成的任務相關的名字,所有的主要框架甚至 JDK 都遵循這個最佳實踐。
- 避免鎖定和縮小同步的範圍
鎖花費的代價高昂且上下文切換更耗費時間空間,試試最低限度的使用同步和鎖,縮小臨界區。因此相對於同步方法我更喜歡同步塊,它給我擁有對鎖的絕對控制權。
- 多用同步類少用
wait
和notify
首先,CountDownLatch
, Semaphore
,CyclicBarrier
和Exchanger
這些同步類簡化了編碼操作,而用 wait 和 notify 很難實現對複雜控制流的控制。其次,這些類是由最好的企業編寫和維護在後續的 JDK 中它們還會不斷優化和完善,使用這些更高等級的同步工具你的程序可以不費吹灰之力獲得優化。
- 多用併發集合少用同步集合
這是另外一個容易遵循且受益巨大的最佳實踐,併發集合比同步集合的可擴展性更好,所以在併發編程時使用併發集合效果更好。如果下一次你需要用到 map,你應該首先想到用ConcurrentHashMap
。
50、 如何強制啓動一個線程?
這個問題就像是如何強制進行 Java 垃圾回收,目前還沒有覺得方法,雖然你可以使用System.gc()
來進行垃圾回收,但是不保證能成功。在 Java 裏面沒有辦法強制啓動一個線程,它是被線程調度器控制着且 Java 沒有公佈相關的API
。
51、 Java 中的 fork join 框架是什麼?
fork join
框架是 JDK7 中出現的一款高效的工具,Java
開發人員可以通過它充分利用現代服務器上的多處理器。它是專門爲了那些可以遞歸劃分成許多子模塊設計的,目的是將所有可用的處理能力用來提升程序的性能。f
Fork join
框架一個巨大的優勢是它使用了工作竊取算法,可以完成更多任務的工作線程可以從其它線程中竊取任務來執行。
52、Java 多線程中調用 wait() 和 sleep() 方法有什麼不同?
Java 程序中 wait 和 sleep 都會造成某種形式的暫停,它們可以滿足不同的需要。wait()
方法用於線程間通信,如果等待條件爲真且其它線程被喚醒時它會釋放鎖,而sleep()
方法僅僅釋放CPU
資源或者讓當前線程停止執行一段時間,但不會釋放鎖。需要注意的是,sleep()
並不會讓線程終止,一旦從休眠中喚醒線程,線程的狀態將會被改變爲Runnable
,並且根據線程調度,它將得到執行。
53、什麼是 Thread Group?爲什麼不建議使用它?
ThreadGroup
是一個類,它的目的是提供關於線程組的信息。
ThreadGroup API
比較薄弱,它並沒有比 Thread 提供了更多的功能。它有兩個主要的功能:一是獲取線程組中處於活躍狀態線程的列表;二是設置爲線程設置未捕獲異常處理器 (ncaught exception handler
)。但在 Java 1.5 中 Thread 類也添加了setUncaughtExceptionHandler(UncaughtExceptionHandler eh)
方法,所以ThreadGroup
是已經過時的,不建議繼續使用。
54、 什麼是 Java 線程轉儲 (Thread Dump),如何得到它?
線程轉儲是一個JVM
活動線程的列表,它對於分析系統瓶頸和死鎖非常有用。有很多方法可以獲取線程轉儲——使用Profiler
,Kill -3
命令,jstack
工具等等。我們更喜歡jstack
工具,因爲它容易使用並且是 JDK 自帶的。由於它是一個基於終端的工具,所以我們可以編寫一些腳本去定時的產生線程轉儲以待分析。
55、 什麼是 Java Timer 類?如何創建一個有特定時間間隔的任務?
java.util.Timer
是一個工具類,可以用於安排一個線程在未來的某個特定時間執行。Timer
類可以用安排一次性任務或者週期任務。
java.util.TimerTask
是一個實現了Runnable
接口的抽象類,我們需要去繼承這個類來創建我們自己的定時任務並使用 Timer 去安排它的執行。
56、什麼是原子操作?在 Java Concurrency API 中有哪些原子類 (atomic classes)?
原子操作是指一個不受其他操作影響的操作任務單元。原子操作是在多線程環境下避免數據不一致必須的手段。
int++
並不是一個原子操作,所以當一個線程讀取它的值並加 1 時,另外一個線程有可能會讀到之前的值,這就會引發錯誤。
在 java.util.concurrent.atomic
包中添加原子變量類之後,這種情況才發生了改變。所有原子變量類都公開比較並設置原語(與比較並交換類似),這些原語都是使用平臺上可用的最快本機結構(比較並交換、加載鏈接 / 條件存儲,最壞的情況下是旋轉鎖)來實現的。java.util.concurrent.atomic
包中提供了原子變量的 9 種風格(AtomicInteger
;AtomicLong
;AtomicReference
;AtomicBoolean
;原子整型;長型;引用;及原子標記引用和戳記引用類的數組形式,其原子地更新一對值)。
57、 Java Concurrency API 中的 Lock 接口 (Lock interface) 是什麼?對比同步它有什麼優勢?
Lock 接口比同步方法和同步塊提供了更具擴展性的鎖操作。他們允許更靈活的結構,可以具有完全不同的性質,並且可以支持多個相關類的條件對象。
它的優勢有:
-
可以使鎖更公平
-
可以使線程在等待鎖的時候響應中斷
-
可以讓線程嘗試獲取鎖,並在無法獲取鎖的時候立即返回或者等待一段時間
-
可以在不同的範圍,以不同的順序獲取和釋放鎖
58、 什麼是 Executor 框架?
Executor
框架同java.util.concurrent.Executor
接口在 Java 5 中被引入。Executor
框架是一個根據一組執行策略調用,調度,執行和控制的異步任務的框架。
無限制的創建線程會引起應用程序內存溢出。所以創建一個線程池是個更好的的解決方案,因爲可以限制線程的數量並且可以回收再利用這些線程。利用Executor
框架可以非常方便的創建一個線程池。
59、Executors 類是什麼?
Executors
爲Executor
,ExecutorService
,ScheduledExecutorService
,ThreadFactory
和Callable
類提供了一些工具方法。
Executors 可以用於方便的創建線程池。
60、 什麼是阻塞隊列?如何使用阻塞隊列來實現生產者 - 消費者模型?
java.util.concurrent.BlockingQueue
的特性是:當隊列是空的時,從隊列中獲取或刪除元素的操作將會被阻塞,或者當隊列是滿時,往隊列裏添加元素的操作會被阻塞。
阻塞隊列不接受空值,當你嘗試向隊列中添加空值的時候,它會拋出NullPointerException
。
阻塞隊列的實現都是線程安全的,所有的查詢方法都是原子的並且使用了內部鎖或者其他形式的併發控制。
BlockingQueue
接口是java collections
框架的一部分,它主要用於實現生產者 - 消費者問題。
61、什麼是 Callable 和 Future?
Java 5
在concurrency
包中引入了java.util.concurrent.Callable
接口,它和Runnable
接口很相似,但它可以返回一個對象或者拋出一個異常。
Callable
接口使用泛型去定義它的返回類型。Executors 類提供了一些有用的方法去在線程池中執行 Callable 內的任務。由於 Callable 任務是並行的,我們必須等待它返回的結果。java.util.concurrent.Future
對象爲我們解決了這個問題。在線程池提交 Callable 任務後返回了一個 Future 對象,使用它我們可以知道 Callable 任務的狀態和得到 Callable 返回的執行結果。Future 提供了 get() 方法讓我們可以等待 Callable 結束並獲取它的執行結果。
62 什麼是 FutureTask?
FutureTask
包裝器是一種非常便利的機制,可將Callable
轉換成Future
和Runnable,
它同時實現兩者的接口。
FutureTask
類是 Future 的一個實現,並實現了Runnable
,所以可通過 Excutor(線程池) 來執行。也可傳遞給 Thread 對象執行。如果在主線程中需要執行比較耗時的操作時,但又不想阻塞主線程時,可以把這些作業交給 Future 對象在後臺完成,當主線程將來需要時,就可以通過Future
對象獲得後臺作業的計算結果或者執行狀態。
63、 什麼是併發容器的實現?
Java 集合類都是快速失敗的,這就意味着當集合被改變且一個線程在使用迭代器遍歷集合的時候,迭代器的 next() 方法將拋出ConcurrentModificationException
異常。
併發容器:併發容器是針對多個線程併發訪問設計的,在 jdk5.0 引入了 concurrent 包,其中提供了很多併發容器,如ConcurrentHashMap
,CopyOnWriteArrayList
等。
併發容器使用了與同步容器完全不同的加鎖策略來提供更高的併發性和伸縮性,例如: 在ConcurrentHashMap
中採用了一種粒度更細的加鎖機制,可以稱爲分段鎖,在這種鎖機制下,允許任意數量的讀線程併發地訪問 map,並且執行讀操作的線程和寫操作的線程也可以併發的訪問 map,同時允許一定數量的寫操作線程併發地修改 map,所以它可以在併發環境下實現更高的吞吐量。
64、用戶線程和守護線程有什麼區別?
當我們在 Java 程序中創建一個線程,它就被稱爲用戶線程。一個守護線程是在後臺執行並且不會阻止JVM
終止的線程。當沒有用戶線程在運行的時候,JVM 關閉程序並且退出。一個守護線程創建的子線程依然是守護線程。
65、有哪些不同的線程生命週期?
當我們在 Java 程序中新建一個線程時,它的狀態是 New。當我們調用線程的 start() 方法時,狀態被改變爲 Runnable。線程調度器會爲Runnable
線程池中的線程分配 CPU 時間並且講它們的狀態改變爲Running
。其他的線程狀態還有Waiting
,Blocked
和Dead
。
66、線程之間是如何通信的?
當線程間是可以共享資源時,線程間通信是協調它們的重要的手段。Object 類中wait()\notify()\notifyAll()
方法可以用於線程間通信關於資源的鎖的狀態。
67、爲什麼 Thread 類的 sleep() 和 yield() 方法是靜態的?
Thread 類的 sleep() 和 yield() 方法將在當前正在執行的線程上運行。所以在其他處於等待狀態的線程上調用這些方法是沒有意義的。這就是爲什麼這些方法是靜態的。它們可以在當前正在執行的線程中工作,並避免程序員錯誤的認爲可以在其他非運行線程調用這些方法。
68、如何確保線程安全?
在 Java 中可以有很多方法來保證線程安全——同步,使用原子類 (atomic concurrent classes
),實現併發鎖,使用 volatile 關鍵字,使用不變類和線程安全類。
69、同步方法和同步塊,哪個是更好的選擇?
同步塊是更好的選擇,因爲它不會鎖住整個對象(當然你也可以讓它鎖住整個對象)。同步方法會鎖住整個對象,哪怕這個類中有多個不相關聯的同步塊,這通常會導致他們停止執行並需要等待獲得這個對象上的鎖。
70、如何創建守護線程?
使用 Thread 類的setDaemon(true)
方法可以將線程設置爲守護線程,需要注意的是,需要在調用start()
方法前調用這個方法,否則會拋出IllegalThreadStateException
異常。
71、線程調度策略?
(1) 搶佔式調度策略
Java 運行時系統的線程調度算法是搶佔式的 (preemptive
)。Java 運行時系統支持一種簡單的固定優先級的調度算法。如果一個優先級比其他任何處於可運行狀態的線程都高的線程進入就緒狀態,那麼運行時系統就會選擇該線程運行。新的優先級較高的線程搶佔 (preempt
) 了其他線程。但是 Java 運行時系統並不搶佔同優先級的線程。換句話說,Java 運行時系統不是分時的 (time-slice
)。然而,基於Java Thread
類的實現系統可能是支持分時的,因此編寫代碼時不要依賴分時。當系統中的處於就緒狀態的線程都具有相同優先級時,線程調度程序採用一種簡單的、非搶佔式的輪轉的調度順序。
(2) 時間片輪轉調度策略
有些系統的線程調度採用時間片輪轉 (round-robin
) 調度策略。這種調度策略是從所有處於就緒狀態的線程中選擇優先級最高的線程分配一定的 CPU 時間運行。該時間過後再選擇其他線程運行。只有當線程運行結束、放棄 (yield
)CPU 或由於某種原因進入阻塞狀態,低優先級的線程纔有機會執行。如果有兩個優先級相同的線程都在等待 CPU,則調度程序以輪轉的方式選擇運行的線程。
72 、在線程中你怎麼處理不可捕捉異常?
Thread.UncaughtExceptionHandler
是java SE5
中的新接口,它允許我們在每一個Thread
對象上添加一個異常處理器。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/uyvw836Mbukneb4VYaIjMQ