Java併發多線程高頻面試題


併發知識無論在學習、面試仍是工做過程當中都很是很是重要,看完本文相信絕對能助你一臂之力。java

一、線程和進程有什麼區別?

線程是進程的子集,一個進程能夠有不少線程。每一個進程都有本身的內存空間,可執行代碼和惟一進程標識符(PID)。程序員

每條線程並行執行不一樣的任務。不一樣的進程使用不一樣的內存空間(線程本身的堆棧),而全部的線程共享一片相同的內存空間(進程主內存)。別把它和棧內存搞混,每一個線程都擁有單獨的棧內存用來存儲本地數據。web

二、實現多線程的方式有哪些?

  • 繼承Thread類:Java單繼承,不推薦;面試

  • 實現Runnable接口:Thread類也是繼承Runnable接口,推薦;算法

  • 實現Callable接口:實現Callable接口,配合FutureTask使用,有返回值;數據庫

  • 使用線程池:複用,節約資源;編程

  • 更多方式能夠參考個人文章使用Java Executor框架實現多線程windows

三、用Runnable仍是Thread?

這個問題是上題的後續,你們都知道咱們能夠經過繼承Thread類或者調用Runnable接口來實現線程,問題是,那個方法更好呢?什麼狀況下使用它?這個問題很容易回答,若是你知道Java不支持類的多重繼承,但容許你調用多個接口。因此若是你要繼承其餘類,固然是調用Runnable接口好了。數組

  • Runnable和Thread二者最大的區別是Thread是類而Runnable是接口,至於用類仍是用接口,取決於繼承上的實際須要。Java類是單繼承的,實現多個接口能夠實現相似多繼承的操做。緩存

  • 其次, Runnable就至關於一個做業,而Thread纔是真正的處理線程,咱們須要的只是定義這個做業,而後將做業交給線程去處理,這樣就達到了鬆耦合,也符合面向對象裏面組合的使用,另外也節省了函數開銷,繼承Thread的同時,不只擁有了做業的方法run(),還繼承了其餘全部的方法。

  • 當須要建立大量線程的時候,有如下不足:①線程生命週期的開銷很是高;②資源消耗;③穩定性。

  • 若是兩者均可以選擇不用,那就不用。由於Java這門語言發展到今天,在語言層面提供的多線程機制已經比較豐富且高級,徹底不用在線程層面操做。直接使用Thread和Runnable這樣的「裸線程」元素比較容易出錯,還須要額外關注線程數等問題。建議:簡單的多線程程序,使用Executor。複雜的多線程程序,使用一個Actor庫,首推Akka。

  • 若是必定要在Runnable和Thread中選擇一個使用,選擇Runnable。

四、Thread 類中的start() 和 run() 方法有什麼區別?

這個問題常常被問到,但仍是能今後區分出面試者對Java線程模型的理解程度。start()方法被用來啓動新建立的線程,並且start()內部調用了run()方法,JDK 1.8源碼中start方法的註釋這樣寫到:Causes this thread to begin execution; the Java Virtual Machine calls the <code>run</code> method of this thread.這和直接調用run()方法的效果不同。當你調用run()方法的時候,只會是在原來的線程中調用,沒有新的線程啓動,start()方法纔會啓動新線程,JDK 1.8源碼中註釋這樣寫:The result is that two threads are running concurrently: the current thread (which returns from the call to the <code>start</code> method) and the other thread (which executes its <code>run</code> method).。

new 一個 Thread,線程進入了新建狀態;調用 start() 方法,會啓動一個線程並使線程進入了就緒狀態,當分配到時間片後就能夠開始運行了。start() 會執行線程的相應準備工做,而後自動執行 run() 方法的內容,這是真正的多線程工做。而直接執行 run() 方法,會把 run 方法當成一個 main 線程下的普通方法去執行,並不會在某個線程中執行它,因此這並非多線程工做。

總結:調用 start 方法方可啓動線程並使線程進入就緒狀態,而 run 方法只是 thread 的一個普通方法調用,仍是在主線程裏執行。

五、說說 sleep() 方法和 wait() 方法區別和共同點?

  • 二者最主要的區別在於:sleep 方法沒有釋放鎖,而 wait 方法釋放了鎖 。

  • 二者均可以暫停線程的執行。

  • Wait 一般被用於線程間交互/通訊,sleep 一般被用於暫停執行。

  • wait() 方法被調用後,線程不會自動甦醒,須要別的線程調用同一個對象上的 notify() 或者 notifyAll() 方法。sleep() 方法執行完成後,線程會自動甦醒。

六、說說併發與並行的區別?

  • 併發:同一時間段,多個任務都在執行 (單位時間內不必定同時執行);

  • 並行:單位時間內,多個任務同時執行。

七、說說線程的生命週期和狀態?

Java 線程在運行的生命週期中的指定時刻只可能處於下面 6 種不一樣狀態的其中一個狀態(圖源《Java 併發編程藝術》4.1.4 節)。

線程在生命週期中並非固定處於某一個狀態而是隨着代碼的執行在不一樣狀態之間切換。Java 線程狀態變遷以下圖所示(圖源《Java 併發編程藝術》4.1.4 節):

由上圖能夠看出:線程建立以後它將處於 NEW(新建) 狀態,調用 start() 方法後開始運行,線程這時候處於 READY(可運行) 狀態。可運行狀態的線程得到了 CPU 時間片(timeslice)後就處於 RUNNING(運行) 狀態。

操做系統隱藏 Java 虛擬機(JVM)中的 RUNNABLE 和 RUNNING 狀態,它只能看到 RUNNABLE 狀態(圖源:HowToDoInJava:Java Thread Life Cycle and Thread States),因此 Java 系統通常將這兩個狀態統稱爲 RUNNABLE(運行中) 狀態 。

當線程執行 wait()方法以後,線程進入 WAITING(等待)狀態。進入等待狀態的線程須要依靠其餘線程的通知纔可以返回到運行狀態,而 TIME_WAITING(超時等待) 狀態至關於在等待狀態的基礎上增長了超時限制,好比經過 sleep(long millis)方法或 wait(long millis)方法能夠將 Java 線程置於 TIMED WAITING 狀態。當超時時間到達後 Java 線程將會返回到 RUNNABLE 狀態。當線程調用同步方法時,在沒有獲取到鎖的狀況下,線程將會進入到 BLOCKED(阻塞) 狀態。線程在執行 Runnable 的run()方法以後將會進入到 TERMINATED(終止) 狀態。

八、什麼是線程死鎖?

多個線程同時被阻塞,它們中的一個或者所有都在等待某個資源被釋放。因爲線程被無限期地阻塞,所以程序不可能正常終止。

以下圖所示,線程 A 持有資源 2,線程 B 持有資源 1,他們同時都想申請對方的資源,因此這兩個線程就會互相等待而進入死鎖狀態。

下面經過一個例子來講明線程死鎖,代碼模擬了上圖的死鎖的狀況 (代碼來源於《併發編程之美》):

public class DeadLockDemo {
private static Object resource1 = new Object();//資源 1
private static Object resource2 = new Object();//資源 2

public static void main(String[] args) {
new Thread(() -> {
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource2");
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
}
}
}, "線程 1").start();

new Thread(() -> {
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource1");
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
}
}
}, "線程 2").start();
}
}

輸出:

Thread[線程 1,5,main]get resource1
Thread[線程 2,5,main]get resource2
Thread[線程 1,5,main]waiting get resource2
Thread[線程 2,5,main]waiting get resource1

線程 A 經過 synchronized (resource1) 得到 resource1 的監視器鎖,而後經過 Thread.sleep(1000);讓線程 A 休眠 1s 爲的是讓線程 B 獲得執行而後獲取到 resource2 的監視器鎖。線程 A 和線程 B 休眠結束了都開始企圖請求獲取對方的資源,而後這兩個線程就會陷入互相等待的狀態,這也就產生了死鎖。上面的例子符合產生死鎖的四個必要條件。

學過操做系統的朋友都知道產生死鎖必須具有如下四個條件:

  1. 互斥條件:該資源任意一個時刻只由一個線程佔用。

  2. 請求與保持條件:一個進程因請求資源而阻塞時,對已得到的資源保持不放。

  3. 不剝奪條件:線程已得到的資源在末使用完以前不能被其餘線程強行剝奪,只有本身使用完畢後才釋放資源。

  4. 循環等待條件:若干進程之間造成一種頭尾相接的循環等待資源關係。

九、如何避免線程死鎖?

咱們只要破壞產生死鎖的四個條件中的其中一個就能夠了。

  • 破壞互斥條件:這個條件咱們沒有辦法破壞,由於咱們用鎖原本就是想讓他們互斥的(臨界資源須要互斥訪問)。

  • 破壞請求與保持條件:一次性申請全部的資源。

  • 破壞不剝奪條件:佔用部分資源的線程進一步申請其餘資源時,若是申請不到,能夠主動釋放它佔有的資源。

  • 破壞循環等待條件:靠按序申請資源來預防。按某一順序申請資源,釋放資源則反序釋放。破壞循環等待條件。

咱們對線程 2 的代碼修改爲下面這樣就不會產生死鎖了。

new Thread(() -> {
synchronized (resource1) {
System.out.println(Thread.currentThread() + "get resource1");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(Thread.currentThread() + "waiting get resource2");
synchronized (resource2) {
System.out.println(Thread.currentThread() + "get resource2");
}
}
}, "線程 2").start();

輸出:

Thread[線程 1,5,main]get resource1
Thread[線程 1,5,main]waiting get resource2
Thread[線程 1,5,main]get resource2
Thread[線程 2,5,main]get resource1
Thread[線程 2,5,main]waiting get resource2
Thread[線程 2,5,main]get resource2

Process finished with exit code 0

咱們分析一下上面的代碼爲何避免了死鎖的發生?

線程 1 首先得到到 resource1 的監視器鎖,這時候線程 2 就獲取不到了。而後線程 1 再去獲取 resource2 的監視器鎖,能夠獲取到。而後線程 1 釋放了對 resource一、resource2 的監視器鎖的佔用,線程 2 獲取到就能夠執行了。這樣就破壞了破壞循環等待條件,所以避免了死鎖。

十、什麼是死鎖,活鎖?

  • 死鎖:多個線程都沒法得到資源繼續執行。能夠經過避免一個線程獲取多個鎖;一個鎖佔用一個資源;使用定時鎖;數據庫加解鎖在一個鏈接中。

  • 死鎖的必要條件:環路等待,不可剝奪,請求保持,互斥條件

  • 活鎖:線程之間相互謙讓資源,都沒法獲取全部資源繼續執行。

十一、Java中CyclicBarrier 和 CountDownLatch有什麼不一樣?

CyclicBarrier 和 CountDownLatch 均可以用來讓一組線程等待其它線程。與 CyclicBarrier 不一樣的是,CountdownLatch 不能從新使用。

  • CountDownLatch是一種靈活的閉鎖實現,能夠使一個或者多個線程等待一組事件發生。閉鎖狀態包括一個計數器,改計數器初始化爲一個正數,表示須要等待的事件數量。countDown方法遞減計數器,表示有一個事件發生了,而await方法等待計數器到達0,表示全部須要等待的事情都已經發生。若是計數器的值非零,那麼await就會一直阻塞知道計數器的值爲0,或者等待的線程中斷,或者等待超時。

  • CyclicBarrier適用於這樣的狀況:你但願建立一組任務,他們並行地執行工做,而後在進行下一個步驟以前等待,直至全部任務都完成。它使得全部的並行任務都將在柵欄出列隊,所以能夠一致的向前移動。這很是像CountDownLatch,只是CountDownLatch是隻觸發一次的事件,而CyclicBarrier能夠屢次重用。

十二、Java中的同步集合與併發集合有什麼區別?

  • 同步集合與併發集合都爲多線程和併發提供了合適的線程安全的集合,不過併發集合的可擴展性更高。在Java1.5以前程序員們只有同步集合來用且在多線程併發的時候會致使爭用,阻礙了系統的擴展性。Java5介紹了併發集合像ConcurrentHashMap,不只提供線程安全還用鎖分離和內部分區等現代技術提升了可擴展性。

  • 同步容器是線程安全的。同步容器將全部對容器狀態的訪問都串行化,以實現他們的線程安全性。這種方法的代價是嚴重下降併發性,當多個線程競爭容器的鎖時,吞吐量將嚴重下降。併發容器是針對多個線程併發訪問設計的,改進了同步容器的性能。經過併發容器來代替同步容器,能夠極大地提升伸縮性並下降風險。

1三、你如何在Java中獲取線程堆棧?

對於不一樣的操做系統,有多種方法來得到Java進程的線程堆棧。當你獲取線程堆棧時,JVM會把全部線程的狀態存到日誌文件或者輸出到控制檯。在Windows你能夠使用Ctrl + Break組合鍵來獲取線程堆棧,Linux下用kill -3命令。你也能夠用jstack這個工具來獲取,它對線程id進行操做,你能夠用jps這個工具找到id。

1四、Java中ConcurrentHashMap的併發度是什麼?

  • ConcurrentHashMap把實際map劃分紅若干部分來實現它的可擴展性和線程安全。這種劃分是使用併發度得到的,它是ConcurrentHashMap類構造函數的一個可選參數,默認值爲16,這樣在多線程狀況下就能避免爭用。

  • 併發度能夠理解爲程序運行時可以同時更新ConccurentHashMap且不產生鎖競爭的最大線程數,實際上就是ConcurrentHashMap中的分段鎖個數,即Segment[]的數組長度。ConcurrentHashMap默認的併發度爲16,但用戶也能夠在構造函數中設置併發度。當用戶設置併發度時,ConcurrentHashMap會使用大於等於該值的最小2冪指數做爲實際併發度(假如用戶設置併發度爲17,實際併發度則爲32)。運行時經過將key的高n位(n = 32 – segmentShift)和併發度減1(segmentMask)作位與運算定位到所在的Segment。segmentShift與segmentMask都是在構造過程當中根據concurrency level被相應的計算出來。

  • 若是併發度設置的太小,會帶來嚴重的鎖競爭問題;若是併發度設置的過大,本來位於同一個Segment內的訪問會擴散到不一樣的Segment中,CPU cache命中率會降低,從而引發程序性能降低。

1五、Java中的同步集合與併發集合有什麼區別?

  • 同步集合與併發集合都爲多線程和併發提供了合適的線程安全的集合,不過併發集合的可擴展性更高。在Java1.5以前程序員們只有同步集合來用且在多線程併發的時候會致使爭用,阻礙了系統的擴展性。Java5介紹了併發集合像ConcurrentHashMap,不只提供線程安全還用鎖分離和內部分區等現代技術提升了可擴展性。

  • 同步容器是線程安全的。同步容器將全部對容器狀態的訪問都串行化,以實現他們的線程安全性。這種方法的代價是嚴重下降併發性,當多個線程競爭容器的鎖時,吞吐量將嚴重下降。併發容器是針對多個線程併發訪問設計的,改進了同步容器的性能。經過併發容器來代替同步容器,能夠極大地提升伸縮性並下降風險。

1六、Thread類中的yield方法有什麼做用?

  • Yield方法能夠暫停當前正在執行的線程對象,讓其它有相同優先級的線程執行。它是一個靜態方法並且只保證當前線程放棄CPU佔用而不能保證使其它線程必定能佔用CPU,執行yield()的線程有可能在進入到暫停狀態後立刻又被執行。

  • 線程讓步:若是知道已經完成了在run()方法的循環的一次迭代過程當中所需的工做,就能夠給線程調度機制一個暗示:你的工做已經作得差很少了,可讓別的線程使用CPU了。這個暗示將經過調用yield()方法來作出(不過這只是一個暗示,沒有任何機制保證它將會被採納)。當調用yield()時,也是在建議具備相同優先級的其餘線程能夠運行。

  • yield()的做用是讓步。它能讓當前線程由「運行狀態」進入到「就緒狀態」,從而讓其它具備相同優先級的等待線程獲取執行權;可是,並不能保證在當前線程調用yield()以後,其它具備相同優先級的線程就必定能得到執行權;也有多是當前線程又進入到「運行狀態」繼續運行!

1七、什麼是ThreadLocal變量?

ThreadLocal是Java裏一種特殊的變量。每一個線程都有一個ThreadLocal就是每一個線程都擁有了本身獨立的一個變量,競爭條件被完全消除了。它是爲建立代價高昂的對象獲取線程安全的好方法,好比你能夠用ThreadLocal讓SimpleDateFormat變成線程安全的,由於那個類建立代價高昂且每次調用都須要建立不一樣的實例因此不值得在局部範圍使用它,若是爲每一個線程提供一個本身獨有的變量拷貝,將大大提升效率。首先,經過複用減小了代價高昂的對象的建立個數。其次,你在沒有使用高代價的同步或者不變性的狀況下得到了線程安全。線程局部變量的另外一個不錯的例子是ThreadLocalRandom類,它在多線程環境中減小了建立代價高昂的Random對象的個數。

ThreadLocal是一種線程封閉技術。ThreadLocal提供了get和set等訪問接口或方法,這些方法爲每一個使用該變量的線程都存有一份獨立的副本,所以get老是返回由當前執行線程在調用set時設置的最新值。

1八、Java內存模型是什麼?

Java內存模型規定和指引Java程序在不一樣的內存架構、CPU和操做系統間有肯定性地行爲。它在多線程的狀況下尤爲重要。Java內存模型對一個線程所作的變更能被其它線程可見提供了保證,它們之間是先行發生關係。這個關係定義了一些規則讓程序員在併發編程時思路更清晰。好比,先行發生關係確保了:

  • 線程內的代碼可以按前後順序執行,這被稱爲程序次序規則。

  • 對於同一個鎖,一個解鎖操做必定要發生在時間上後發生的另外一個鎖定操做以前,也叫作管程鎖定規則。

  • 前一個對volatile的寫操做在後一個volatile的讀操做以前,也叫volatile變量規則。

  • 一個線程內的任何操做必需在這個線程的start()調用以後,也叫做線程啓動規則。

  • 一個線程的全部操做都會在線程終止以前,線程終止規則。

  • 一個對象的終結操做必需在這個對象構造完成以後,也叫對象終結規則。

  • 可傳遞性

我強烈建議你們閱讀《Java併發編程實踐》第十六章來加深對Java內存模型的理解。

1九、Java中的volatile 變量是什麼?

volatile是一個特殊的修飾符,只有成員變量才能使用它。在Java併發程序缺乏同步類的狀況下,多線程對成員變量的操做對其它線程是透明的。volatile變量能夠保證下一個讀取操做會在前一個寫操做以後發生,就是上一題的volatile變量規則。

Java語言提供了一種稍弱的同步機制,即volatile變量,用來確保將變量的更新操做通知到其餘線程。當把變量聲明爲volatile類型後,編譯器和運行時都會注意到這個變量是共享的,所以不會將變量上的操做和其餘內存操做一塊兒重排序。volatile變量不會被緩存在寄存器或者對其餘處理器不可見的地方,所以在讀取volatile類型的時候總會返回最新寫入的值。

在訪問volatile變量時不會執行加鎖操做,所以也不會使執行線程阻塞,所以volatile變量是一種比synchronized關鍵字更輕量級的同步機制。

加鎖機制既能夠確保可見性又能夠確保原子性,而volatile變量只能確保可見性。

20、volatile 變量和 atomic 變量有什麼不一樣?

這是個有趣的問題。首先,volatile 變量和 atomic 變量看起來很像,但功能卻不同。Volatile變量能夠確保先行關係,即寫操做會發生在後續的讀操做以前, 但它並不能保證原子性。例如用volatile修飾count變量那麼 count++ 操做就不是原子性的。而AtomicInteger類提供的atomic方法可讓這種操做具備原子性如getAndIncrement()方法會原子性的進行增量操做把當前值加一,其它數據類型和引用變量也能夠進行類似操做。

2一、Java中Runnable和Callable有什麼不一樣?

  • Runnable和Callable都表明那些要在不一樣的線程中執行的任務。Runnable從JDK1.0開始就有了,Callable是在JDK1.5增長的。它們的主要區別是Callable的 call() 方法能夠返回值和拋出異常,而Runnable的run()方法沒有這些功能。Callable能夠返回裝載有計算結果的Future對象。

  • Runnable是執行工做的獨立任務,可是它不返回任何值。若是但願任務在完成的時候可以返回一個值,那麼能夠實現Callable接口而不是Runnable接口。在Java SE5中引入的Callable是一種具備類型參數的泛型,它的類型參數表示的是從方法call()(而不是run())中返回的值,而且必須使用ExecutorService.submit()方法調用它。submit()方法會產生Future對象,它用Callable返回結果的特定類型進行了參數化。

2二、哪些操做釋放鎖,哪些不釋放鎖?

  • sleep(): 釋放資源,不釋放鎖,進入阻塞狀態,喚醒隨機線程,Thread類方法。

  • wait(): 釋放資源,釋放鎖,Object類方法。

  • yield(): 不釋放鎖,進入可執行狀態,選擇優先級高的線程執行,Thread類方法。

  • 若是線程產生的異常沒有被捕獲,會釋放鎖。

2三、如何正確的終止線程?

  • 使用共享變量,要用volatile關鍵字,保證可見性,可以及時終止。

  • 使用interrupt()和isInterrupted()配合使用。

2四、interrupt(), interrupted(), isInterrupted()的區別?

  • interrupt():設置中斷標誌;

  • interrupted():響應中斷標誌並復位中斷標誌;

  • isInterrupted():響應中斷標誌;

2五、synchronized的鎖對象是哪些?

  • 普通方法是當前實例對象;

  • 同步方法快是括號中配置內容,能夠是類Class對象,能夠是實例對象;

  • 靜態方法是當前類Class對象。

  • 只要不是同一個鎖,就能夠並行執行,同一個鎖,只能串行執行。

  • 更多參考個人文章Java中Synchronized關鍵字簡介(譯)

2六、volatile和synchronized的區別是什麼?

  1. volatile只能使用在變量上;而synchronized能夠在類,變量,方法和代碼塊上。

  2. volatile至保證可見性;synchronized保證原子性與可見性。

  3. volatile禁用指令重排序;synchronized不會。

  4. volatile不會形成阻塞;synchronized會。

2七、什麼是緩存一致性協議?

由於CPU是運算很快,而主存的讀寫很忙,因此在程序運行中,會複製一份數據到高速緩存,處理完成在將結果保存主存.

這樣存在一些問題,在多核CPU中多個線程,多個線程拷貝多份的高速緩存數據,最後在計算完成,刷到主存的數據就會出現覆蓋

因此就出現了緩存一致性協議。最出名的就是Intel 的MESI協議,MESI協議保證了每一個緩存中使用的共享變量的副本是一致的。它核心的思想是:當CPU寫數據時,若是發現操做的變量是共享變量,即在其餘CPU中也存在該變量的副本,會發出信號通知其餘CPU將該變量的緩存行置爲無效狀態,所以當其餘CPU須要讀取這個變量時,發現本身緩存中緩存該變量的緩存行是無效的,那麼它就會從內存從新讀取。

2八、Synchronized關鍵字、Lock,並解釋它們之間的區別?

Synchronized 與Lock都是可重入鎖,同一個線程再次進入同步代碼的時候.能夠使用本身已經獲取到的鎖

Synchronized是悲觀鎖機制,獨佔鎖。而Locks.ReentrantLock是,每次不加鎖而是假設沒有衝突而去完成某項操做,若是由於衝突失敗就重試,直到成功爲止。ReentrantLock適用場景

某個線程在等待一個鎖的控制權的這段時間須要中斷

須要分開處理一些wait-notify,ReentrantLock裏面的Condition應用,可以控制notify哪一個線程,鎖能夠綁定多個條件。

具備公平鎖功能,每一個到來的線程都將排隊等候。

2九、Volatile如何保證內存可見性?

  • 當寫一個volatile變量時,JMM會把該線程對應的本地內存中的共享變量刷新到主內存。

  • 當讀一個volatile變量時,JMM會把該線程對應的本地內存置爲無效。線程接下來將從主內存中讀取共享變量。

30、 Java中什麼是競態條件?

競態條件會致使程序在併發狀況下出現一些bugs。多線程對一些資源的競爭的時候就會產生競態條件,若是首先要執行的程序競爭失敗排到後面執行了,那麼整個程序就會出現一些不肯定的bugs。這種bugs很難發現並且會重複出現,由於線程間的隨機競爭。

3一、爲何wait, notify 和 notifyAll這些方法不在thread類裏面?

明顯的緣由是JAVA提供的鎖是對象級的而不是線程級的,每一個對象都有鎖,經過線程得到。若是線程須要等待某些鎖那麼調用對象中的wait()方法就有意義了。若是wait()方法定義在Thread類中,線程正在等待的是哪一個鎖就不明顯了。簡單的說,因爲wait,notify和notifyAll都是鎖級別的操做,因此把他們定義在Object類中由於鎖屬於對象。

3二、Java中synchronized 和 ReentrantLock 有什麼不一樣?

類似點:

這兩種同步方式有不少類似之處,它們都是加鎖方式同步,並且都是阻塞式的同步,也就是說當若是一個線程得到了對象鎖,進入了同步塊,其餘訪問該同步塊的線程都必須阻塞在同步塊外面等待,而進行線程阻塞和喚醒的代價是比較高的.

區別:

這兩種方式最大區別就是對於Synchronized來講,它是java語言的關鍵字,是原生語法層面的互斥,須要jvm實現。而ReentrantLock它是JDK 1.5以後提供的API層面的互斥鎖,須要lock()和unlock()方法配合try/finally語句塊來完成。

Synchronized進過編譯,會在同步塊的先後分別造成monitorenter和monitorexit這個兩個字節碼指令。在執行monitorenter指令時,首先要嘗試獲取對象鎖。若是這個對象沒被鎖定,或者當前線程已經擁有了那個對象鎖,把鎖的計算器加1,相應的,在執行monitorexit指令時會將鎖計算器就減1,當計算器爲0時,鎖就被釋放了。若是獲取對象鎖失敗,那當前線程就要阻塞,直到對象鎖被另外一個線程釋放爲止。

因爲ReentrantLock是java.util.concurrent包下提供的一套互斥鎖,相比Synchronized,ReentrantLock類提供了一些高級功能,主要有如下3項:

  • 等待可中斷,持有鎖的線程長期不釋放的時候,正在等待的線程能夠選擇放棄等待,這至關於Synchronized來講能夠避免出現死鎖的狀況。

  • 公平鎖,多個線程等待同一個鎖時,必須按照申請鎖的時間順序得到鎖,Synchronized鎖非公平鎖,ReentrantLock默認的構造函數是建立的非公平鎖,能夠經過參數true設爲公平鎖,但公平鎖表現的性能不是很好。

  • 鎖綁定多個條件,一個ReentrantLock對象能夠同時綁定對個對象。

3三、synchronized 用過嗎,其原理是什麼?

這是一道 Java 面試中幾乎百分百會問到的問題,由於只要是程序員就必定會經過或者接觸過 synchronized。

答:synchronized 是由 JVM 實現的一種實現互斥同步的一種方式,若是 你查看被 synchronized 修飾過的程序塊編譯後的字節碼,會發現, 被 synchronized 修飾過的程序塊,在編譯先後被編譯器生成了monitorenter 和 monitorexit 兩 個 字 節 碼 指 令 。

這兩個指令是什麼意思呢?

在虛擬機執行到 monitorenter 指令時,首先要嘗試獲取對象的鎖: 若是這個對象沒有鎖定,或者當前線程已經擁有了這個對象的鎖,把鎖 的計數器 +1;當執行 monitorexit 指令時將鎖計數器 -1;當計數器 爲 0 時,鎖就被釋放了。若是獲取對象失敗了,那當前線程就要阻塞等待,直到對象鎖被另一 個線程釋放爲止。

Java 中 Synchronize 經過在對象頭設置標記,達到了獲取鎖和釋放 鎖的目的。

3四、上面提到獲取對象的鎖,這個「鎖」究竟是什麼?如何肯定對象的鎖?

答:「鎖」的本質實際上是 monitorenter 和 monitorexit 字節碼指令的一 個 Reference 類型的參數,即要鎖定和解鎖的對象。咱們知道,使用Synchronized 能夠修飾不一樣的對象,所以,對應的對象鎖能夠這麼確 定:

  1. 若是 Synchronized 明確指定了鎖對象,好比 Synchronized(變量 名)、Synchronized(this) 等,說明加解鎖對象爲該對象。

  2. 若是沒有明確指定:

  • 若 Synchronized 修飾的方法爲非靜態方法,表示此方法對應的對象爲 鎖對象;

  • 若 Synchronized 修飾的方法爲靜態方法,則表示此方法對應的類對象 爲鎖對象。

注意,當一個對象被鎖住時,對象裏面全部用 Synchronized 修飾的 方法都將產生堵塞,而對象裏非 Synchronized 修飾的方法可正常被 調用,不受鎖影響。

3五、什麼是可重入性,爲何說 Synchronized 是可重入鎖?

先來看一下維基百科關於可重入鎖的定義:

若一個程序或子程序能夠「在任意時刻被中斷而後操做系統調度執行另一段代碼,這段代碼又調用了該子程序不會出錯」,則稱其爲可重入(reentrant或re-entrant)的。即當該子程序正在運行時,執行線程能夠再次進入並執行它,仍然得到符合設計時預期的結果。與多線程併發執行的線程安全不一樣,可重入強調對單個線程執行時從新進入同一個子程序仍然是安全的。

通俗來講:當線程請求一個由其它線程持有的對象鎖時,該線程會阻塞,而當線程請求由本身持有的對象鎖時,若是該鎖是重入鎖,請求就會成功,不然阻塞。

要證實synchronized是否是可重入鎖,咱們先來看一段代碼:

package com.mzc.common.concurrent.synchronize;

/**
* <p class="detail">
* 功能: 證實synchronized爲何是可重入鎖
* </p>
*
* @author Moore
* @ClassName Super class.
* @Version V1.0.
* @date 2020.02.07 15:34:12
*/

public class SuperClass {

public synchronized void doSomething(){
System.out.println("father is doing something,the thread name is:"+Thread.currentThread().getName());
}
}

package com.mzc.common.concurrent.synchronize;

/**
* <p class="detail">
* 功能: 證實synchronized爲何是可重入鎖
* </p>
*
* @author Moore
* @ClassName Sub class.
* @Version V1.0.
* @date 2020.02.07 15:34:41
*/

public class SubClass extends SuperClass {

public synchronized void doSomething() {
System.out.println("child is doing doSomething,the thread name is:" + Thread.currentThread().getName());
// 調用本身類中其餘的synchronized方法
doAnotherThing();
}

private synchronized void doAnotherThing() {
// 調用父類的synchronized方法
super.doSomething();
System.out.println("child is doing anotherThing,the thread name is:" + Thread.currentThread().getName());
}

public static void main(String[] args) {
SubClass child = new SubClass();
child.doSomething();
}
}

經過運行main方法,先一下結果:

child is doing doSomething,the thread name is:main
father is doing something,the thread name is:main
child is doing anotherThing,the thread name is:main

由於這些方法輸出了相同的線程名稱,代表即便遞歸使用synchronized也沒有發生死鎖,證實其是可重入的。

還看不懂?那我就再解釋下!

這裏的對象鎖只有一個,就是 child 對象的鎖,當執行 child.doSomething 時,該線程得到 child 對象的鎖,在 doSomething 方法內執行 doAnotherThing 時再次請求child對象的鎖,由於synchronized 是重入鎖,因此能夠獲得該鎖,繼續在 doAnotherThing 裏執行父類的 doSomething 方法時第三次請求 child 對象的鎖,一樣可獲得。若是不是重入鎖的話,那這後面這兩次請求鎖將會被一直阻塞,從而致使死鎖。

因此在 java 內部,同一線程在調用本身類中其餘 synchronized 方法/塊或調用父類的 synchronized 方法/塊都不會阻礙該線程的執行。就是說同一線程對同一個對象鎖是可重入的,並且同一個線程能夠獲取同一把鎖屢次,也就是能夠屢次重入。由於java線程是基於「每線程(per-thread)」,而不是基於「每調用(per-invocation)」的(java中線程得到對象鎖的操做是以線程爲粒度的,per-invocation 互斥體得到對象鎖的操做是以每調用做爲粒度的)。

重入鎖實現可重入性原理或機制是:每個鎖關聯一個線程持有者和計數器,當計數器爲 0 時表示該鎖沒有被任何線程持有,那麼任何線程均可能得到該鎖而調用相應的方法;當某一線程請求成功後,JVM會記下鎖的持有線程,而且將計數器置爲 1;此時其它線程請求該鎖,則必須等待;而該持有鎖的線程若是再次請求這個鎖,就能夠再次拿到這個鎖,同時計數器會遞增;當線程退出同步代碼塊時,計數器會遞減,若是計數器爲 0,則釋放該鎖。

3六、JVM 對 Java 的原生鎖作了哪些優化?

在 Java 6 以前,Monitor 的實現徹底依賴底層操做系統的互斥鎖來實現,也就是咱們剛纔在問題二中所闡述的獲取/釋放鎖的邏輯。

因爲 Java 層面的線程與操做系統的原生線程有映射關係,若是要將一 個線程進行阻塞或喚起都須要操做系統的協助,這就須要從用戶態切換 到內核態來執行,這種切換代價十分昂貴,很耗處理器時間,現代 JDK中作了大量的優化。一種優化是使用自旋鎖,即在把線程進行阻塞操做以前先讓線程自旋等待一段時間,可能在等待期間其餘線程已經解鎖,這時就無需再讓線程 執行阻塞操做,避免了用戶態到內核態的切換。

現代 JDK 中還提供了三種不一樣的 Monitor 實現,也就是三種不一樣的鎖:

  • 偏向鎖(Biased Locking)

  • 輕量級鎖

  • 重量級鎖

這三種鎖使得 JDK 得以優化 Synchronized 的運行,當 JVM 檢測 到不一樣的競爭情況時,會自動切換到適合的鎖實現,這就是鎖的升級、 降級。

  • 當沒有競爭出現時,默認會使用偏向鎖。JVM 會利用 CAS 操做,在對象頭上的 Mark Word 部分設置線程ID,以表示這個對象偏向於當前線程,因此並不涉及真正的互斥鎖,因 爲在不少應用場景中,大部分對象生命週期中最多會被一個線程鎖定, 使用偏斜鎖能夠下降無競爭開銷。

  • 若是有另外一線程試圖鎖定某個被偏斜過的對象,JVM 就撤銷偏斜鎖, 切換到輕量級鎖實現。

  • 輕量級鎖依賴 CAS 操做 Mark Word 來試圖獲取鎖,若是重試成功, 就使用普通的輕量級鎖;不然,進一步升級爲重量級鎖。

3七、爲何說 Synchronized 是非公平鎖?

答:非公平主要表如今獲取鎖的行爲上,並不是是按照申請鎖的時間先後給等待線程分配鎖的,每當鎖被釋放後,任何一個線程都有機會競爭到鎖, 這樣作的目的是爲了提升執行性能,缺點是可能會產生線程飢餓現象。

3八、爲何說 Synchronized 是一個悲觀鎖?樂觀鎖的實現原理 又是什麼?什麼是 CAS,它有什麼特性?

答:Synchronized 顯然是一個悲觀鎖,由於它的併發策略是悲觀的:無論是否會產生競爭,任何的數據操做都必需要加鎖、用戶態核心態轉 換、維護鎖計數器和檢查是否有被阻塞的線程須要被喚醒等操做。

隨着硬件指令集的發展,咱們能夠使用基於衝突檢測的樂觀併發策略。先進行操做,若是沒有其餘線程徵用數據,那操做就成功了; 若是共享數據有徵用,產生了衝突,那就再進行其餘的補償措施。這種 樂觀的併發策略的許多實現不須要線程掛起,因此被稱爲非阻塞同步。

樂觀鎖的核心算法是 CAS(Compareand Swap,比較並交換),它涉 及到三個操做數:內存值、預期值、新值。當且僅當預期值和內存值相 等時纔將內存值修改成新值。這樣處理的邏輯是,首先檢查某塊內存的值是否跟以前我讀取時的一 樣,如不同則表示期間此內存值已經被別的線程更改過,捨棄本次操 做,不然說明期間沒有其餘線程對此內存值操做,能夠把新值設置給此 塊內存。

CAS 具備原子性,它的原子性由CPU 硬件指令實現保證,即便用JNI 調用 Native 方法調用由 C++ 編寫的硬件級別指令,JDK 中提 供了 Unsafe 類執行這些操做。

3九、樂觀鎖必定就是好的嗎?

答:樂觀鎖避免了悲觀鎖獨佔對象的現象,同時也提升了併發性能,但它也 有缺點:

  • 樂觀鎖只能保證一個共享變量的原子操做。若是多一個或幾個變量,樂 觀鎖將變得力不從心,但互斥鎖能輕易解決,無論對象數量多少及對象顆粒度大小。

  • 長時間自旋可能致使開銷大。假如 CAS 長時間不成功而一直自旋,會 給 CPU 帶來很大的開銷。

  • ABA 問題。CAS 的核心思想是經過比對內存值與預期值是否同樣而判 斷內存值是否被改過,但這個判斷邏輯不嚴謹,假如內存值原來是 A, 後來被一條線程改成 B,最後又被改爲了 A,則 CAS 認爲此內存值並 沒有發生改變,但其實是有被其餘線程改過的,這種狀況對依賴過程 值的情景的運算結果影響很大。解決的思路是引入版本號,每次變量更新都把版本號加一

40、談一談AQS框架。

AQS(AbstractQueuedSynchronizer 類)是一個用來構建鎖和同步器 的框架,各類Lock 包中的鎖(經常使用的有 ReentrantLock、 ReadWriteLock) , 以 及 其 他 如 Semaphore、 CountDownLatch, 甚 至是早期的 FutureTask 等,都是基於 AQS 來構建。

  1. AQS 在內部定義了一個 volatile int state 變量,表示同步狀態:當線 程調用 lock 方法時 ,若是 state=0,說明沒有任何線程佔有共享資源 的鎖,能夠得到鎖並將 state=1;若是 state=1,則說明有線程目前正在 使用共享變量,其餘線程必須加入同步隊列進行等待。

  2. AQS 經過 Node 內部類構成的一個雙向鏈表結構的同步隊列,來完成線 程獲取鎖的排隊工做,當有線程獲取鎖失敗後,就被添加到隊列末尾。Node 類是對要訪問同步代碼的線程的封裝,包含了線程自己及其狀態叫waitStatus(有五種不一樣 取值,分別表示是否被阻塞,是否等待喚醒, 是否已經被取消等),每一個 Node 結點關聯其 prev 結點和 next 結 點,方便線程釋放鎖後快速喚醒下一個在等待的線程,是一個 FIFO 的過 程。Node 類有兩個常量,SHARED 和 EXCLUSIVE,分別表明共享模式和獨 佔模式。所謂共享模式是一個鎖容許多條線程同時操做(信號量Semaphore 就是基於 AQS 的共享模式實現的),獨佔模式是同一個時 間段只能有一個線程對共享資源進行操做,多餘的請求線程須要排隊等待 ( 如 ReentranLock) 。

  3. AQS 經過內部類 ConditionObject 構建等待隊列(可有多個),當Condition 調用 wait() 方法後,線程將會加入等待隊列中,而當Condition 調用 signal() 方法後,線程將從等待隊列轉移動同步隊列中進行鎖競爭。

  4. AQS 和 Condition 各自維護了不一樣的隊列,在使用 Lock 和Condition 的時候,其實就是兩個隊列的互相移動。

4一、ReentrantLock 是如何實現可重入性的?

答:ReentrantLock 內部自定義了同步器 Sync (Sync 既實現了 AQS, 又實現了 AOS,而 AOS 提供了一種互斥鎖持有的方式),其實就是 加鎖的時候經過 CAS 算法,將線程對象放到一個雙向鏈表中,每次獲 取鎖的時候,看下當前維護的那個線程 ID 和當前請求的線程 ID 是否 同樣,同樣就可重入了。

4二、Java中Semaphore是什麼?

Java中的Semaphore是一種新的同步類,它是一個計數信號。從概念上講,從概念上講,信號量維護了一個許可集合。若有必要,在許可可用前會阻塞每個 acquire(),而後再獲取該許可。每一個 release()添加一個許可,從而可能釋放一個正在阻塞的獲取者。可是,不使用實際的許可對象,Semaphore只對可用許可的號碼進行計數,並採起相應的行動。信號量經常用於多線程的代碼中,好比數據庫鏈接池。

package com.mzc.common.concurrent;

import java.util.concurrent.Semaphore;

/**
* <p class="detail">
* 功能: Semaphore Test
* </p>
*
* @author Moore
* @ClassName Test semaphore.
* @Version V1.0.
* @date 2020.02.07 20:11:00
*/

public class TestSemaphore {

static class Worker extends Thread{
private int num;
private Semaphore semaphore;
public Worker(int num,Semaphore semaphore){
this.num = num;
this.semaphore = semaphore;
}
@Override
public void run() {
try {
// 搶許可
semaphore.acquire();
Thread.sleep(2000);
// 釋放許可
semaphore.release();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}

public static void main(String[] args) {
// 機器數目,即5個許可
Semaphore semaphore = new Semaphore(5);
// 8個線程去搶許可
for (int i = 0; i < 8; i++){
new Worker(i,semaphore).start();
}
}
}

4三、Java 中的線程池是如何實現的?

  • 在 Java 中,所謂的線程池中的「線程」,實際上是被抽象爲了一個靜態內部類 Worker,它基於 AQS 實現,存放在線程池的HashSet<Worker> workers 成員變量中;

  • 而須要執行的任務則存放在成員變量 workQueue(BlockingQueue<Runnable> workQueue)中。這樣,整個線程池實現的基本思想就是: 從 workQueue 中不斷取出須要執行的任務,放在 Workers 中進行處理。

4四、線程池中的線程是怎麼建立的?是一開始就隨着線程池的啓動建立好的嗎?

答:顯然不是的。線程池默認初始化後不啓動 Worker,等待有請求時才啓動。每當咱們調用 execute() 方法添加一個任務時,線程池會作以下判斷:

  • 若是正在運行的線程數量小於 corePoolSize,那麼立刻建立線程運行這個任務;

  • 若是正在運行的線程數量大於或等於 corePoolSize,那麼將這個任務放入隊列;

  • 若是這時候隊列滿了,並且正在運行的線程數量小於maximumPoolSize,那麼仍是要建立非核心線程馬上運行這個任務;

  • 若是隊列滿了,並且正在運行的線程數量大於或等於maximumPoolSize,那麼線程池會拋出異常RejectExecutionException。

當一個線程完成任務時,它會從隊列中取下一個任務來執行。當一個線程無事可作,超過必定的時間(keepAliveTime)時,線程池會判斷。

若是當前運行的線程數大於 corePoolSize,那麼這個線程就被停掉。因此線程池的全部任務完成後,它最終會收縮到 corePoolSize 的大小。

4五、什麼是競爭條件?如何發現和解決競爭?

兩個線程同步操做同一個對象,使這個對象的最終狀態不明——叫作競爭條件。競爭條件能夠在任何應該由程序員保證原子操做的,而又忘記使用synchronized的地方。

惟一的解決方案就是加鎖。

Java有兩種鎖可供選擇:

  • 對象或者類(class)的鎖。每個對象或者類都有一個鎖。使用synchronized關鍵字獲取。synchronized加到static方法上面就使用類鎖,加到普通方法上面就用對象鎖。除此以外synchronized還能夠用於鎖定關鍵區域塊(Critical Section)。synchronized以後要制定一個對象(鎖的攜帶者),並把關鍵區域用大括號包裹起來。synchronized(this){// critical code}。

  • 顯示構建的鎖(java.util.concurrent.locks.Lock),調用lock的lock方法鎖定關鍵代碼。

4六、不少人都說要慎用 ThreadLocal,談談你的理解,使用ThreadLocal 須要注意些什麼?

答:使 用 ThreadLocal 要 注 意 remove!

ThreadLocal 的實現是基於一個所謂的 ThreadLocalMap,在ThreadLocalMap 中,它的 key 是一個弱引用。一般弱引用都會和引用隊列配合清理機制使用,可是 ThreadLocal 是 個例外,它並無這麼作。這意味着,廢棄項目的回收依賴於顯式地觸發,不然就要等待線程結 束,進而回收相應 ThreadLocalMap! 這就是不少 OOM 的來源,因此一般都會建議,應用必定要本身負責 remove,而且不要和線程池配 合,由於 worker 線程每每是不會退出的。

線程與鎖

哲學家問題
問題描述:五位哲學家圍繞一個圓桌就作,桌上在每兩位哲學家之間擺着一支筷子。哲學家的狀態多是「思考」或者「飢餓」。若是飢餓,哲學家將拿起他兩邊的筷子就餐一段時間。進餐結束後,哲學家就會放回筷子。

代碼實現:

public class Philosopher extends Thread {
private Chopstick left;
private Chopstick right;
private Random random;

public Philosopher(Chopstick left, Chopstick right) {
this.left = left;
this.right = right;
random = new Random();
}

@Override
public void run() {
try {
while (true) {
Thread.sleep(random.nextInt(1000)); // 思考一下子
synchronized (left) { // 拿起左手的筷子
synchronized (right) { // 拿起右手的筷子
Thread.sleep(random.nextInt(1000)); // 進餐
}
}
}
} catch (InterruptedException e) {
// handle exception
}
}
}

規避方法:
一個線程使用多把鎖時,就須要考慮死鎖的可能。幸運的是,若是老是按照一個全局的固定的順序得到多把鎖,就能夠避開死鎖。

public class Philosopher2 extends Thread {
private Chopstick first;
private Chopstick second;
private Random random;

public Philosopher2(Chopstick left, Chopstick right) {
if (left.getId() < right.getId()) {
first = left;
second = right;
} else {
first = right;
second = left;
}
random = new Random();
}

@Override
public void run() {
try {
while (true) {
Thread.sleep(random.nextInt(1000)); // 思考一下子
synchronized (first) { // 拿起左手的筷子
synchronized (second) { // 拿起右手的筷子
Thread.sleep(random.nextInt(1000)); // 進餐
}
}
}
} catch (InterruptedException e) {
// handle exception
}
}
}

外星方法
定義:調用這類方法時,調用者對方法的實現細節並不瞭解。

public class Downloader extends Thread {
private InputStream in;
private OutputStream out;
private ArrayList<ProgressListener> listeners;

public Downloader(URL url, String outputFilename) throws IOException {
in = url.openConnection().getInputStream();
out = new FileOutputStream(outputFilename);
listeners = new ArrayList<>();
}

public synchronized void addListener(ProgressListener listener) {
listeners.add(listener);
}

public synchronized void removeListener(ProgressListener listener) {
listeners.remove(listener);
}

private synchronized void updateProgress(int n) {
for (ProgressListener listener : listeners) {
listener.onProgress(n);
}
}

@Override
public void run() {
// ...
}
}

這裏 updateProgress(n) 方法調用了一個外星方法,這個外星方法可能作任何事,好比持有另一把鎖。

能夠這樣來修改:

private  void updateProgress(int n) {
ArrayList<ProgressListener> listenersCopy;
synchronized (this) {
listenersCopy = (ArrayList<ProgressListener>) listeners.clone();
}

for (ProgressListener listener : listenersCopy) {
listener.onProgress(n);
}
}

線程與鎖模型帶來的三個主要危害:

競態條件
死鎖
內存可見性


規避原則:

對共享變量的全部訪問都須要同步化
讀線程和寫線程都須要同步化
按照約定的全局順序來獲取多把鎖
當持有鎖時避免調用外星方法
持有鎖的時間應儘量短


內置鎖
內置鎖限制:

沒法中斷 一個線程由於等待內置鎖而進入阻塞以後,就沒法中斷該線程了;
沒法超時 嘗試獲取內置鎖時,沒法設置超時;
不靈活 得到內置鎖,必須使用 synchronized 塊。

synchronized( object ) {
<<使用共享資源>>
}

ReentrantLock
其提供了顯式的lock和unlock, 能夠突破以上內置鎖的幾個限制。

ReentrantLock lock = new ReentrantLock();
lock.lock();
try {
<<使用共享資源>>
} finally {
lock.unlock()
}

可中斷
使用內置鎖時,因爲阻塞的線程沒法被中斷,程序不可能從死鎖中恢復。

內置鎖:製造一個死鎖:

public class Uninterruptible {

public static void main(String[] args) throws InterruptedException {
final Object o1 = new Object();
final Object o2 = new Object();

Thread t1 = new Thread(){
@Override
public void run() {
try {
synchronized (o1) {
Thread.sleep(1000);
synchronized (o2) {}
}
} catch (InterruptedException e) {
System.out.println("Thread-1 interrupted");
}
}
};

Thread t2 = new Thread(){
@Override
public void run() {
try {
synchronized (o2) {
Thread.sleep(1000);
synchronized (o1) {}
}
} catch (InterruptedException e) {
System.out.println("Thread-2 interrupted");
}
}
};

t1.start();
t2.start();
Thread.sleep(2000);
t1.interrupt();
t2.interrupt();
t1.join();
t2.join();
}

}

ReentrantLock 替代內置鎖:

public class Interruptible {

public static void main(String[] args) {
final ReentrantLock lock1 = new ReentrantLock();
final ReentrantLock lock2 = new ReentrantLock();

Thread t1 = new Thread(){
@Override
public void run() {
try {
lock1.lockInterruptibly();
Thread.sleep(1000);
lock2.lockInterruptibly();
} catch (InterruptedException e) {
System.out.println("Thread-1 interrupted");
}
}
};

// ...
}
}

可超時
利用 ReentrantLock 超時設置解決哲學家問題:

public class Philosopher3 extends Thread {
private ReentrantLock leftChopstick;
private ReentrantLock rightChopstick;
private Random random;

public Philosopher3(ReentrantLock leftChopstick, ReentrantLock rightChopstick) {
this.leftChopstick = leftChopstick;
this.rightChopstick = rightChopstick;
random = new Random();
}

@Override
public void run() {
try {
while (true) {
Thread.sleep(random.nextInt(1000)); // 思考一下子
leftChopstick.lock();
try {
// 獲取右手邊的筷子
if (rightChopstick.tryLock(1000, TimeUnit.MILLISECONDS)) {
try {
Thread.sleep(random.nextInt(1000));
} finally {
rightChopstick.unlock();
}
} else {
// 沒有獲取到右手邊的筷子,放棄並繼續思考
}
} finally {
leftChopstick.unlock();
}
}
} catch (InterruptedException e) {
// ...
}
}
}

交替鎖

場景:在鏈表中插入一個節點時,使用交替鎖只鎖住鏈表的一部分,而不是用鎖保護整個鏈表。

線程安全鏈表:

public class ConcurrentSortedList {  // 降序有序鏈表

private class Node {
int value;
Node pre;
Node next;

ReentrantLock lock = new ReentrantLock();

Node() {}

Node(int value, Node pre, Node next) {
this.value = value;
this.pre = pre;
this.next = next;
}
}

private final Node head;
private final Node tail;

public ConcurrentSortedList() {
this.head = new Node();
this.tail = new Node();
this.head.next = tail;
this.tail.pre = head;
}

public void insert(int value) {
Node current = this.head;
current.lock.lock();
Node next = current.next;
try {
while (true) {
next.lock.lock();
try {
if (next == tail || next.value < value) {
Node newNode = new Node(value, current, next);
next.pre = newNode;
current.next = newNode;
return;
}
} finally {
current.lock.unlock();
}
current = next;
next = current.next;

}
} finally {
next.lock.unlock();
}
}

public int size() {
Node current = tail; // 這裏爲何要是從尾部開始遍歷呢?由於插入是從頭部開始遍歷的
int count = 0;
while (current != head) {
ReentrantLock lock = current.lock;
lock.lock();
try {
++count;
current = current.pre;
} finally {
lock.unlock();
}
}
return count;
}
}

條件變量

併發編程常常要等待某個條件知足。好比從隊列刪除元素必須等待隊列不爲空、向緩存添加數據前須要等待緩存有足夠的空間。

條件變量模式:

ReentrantLock lock = new ReentrantLock();
Condition condition = lock.newConditiion();

lock.lock();
try {
while(!<<條件爲真>>) { // 條件不爲真時
condition.await();
}
<<使用共享資源>>
} finnally {
lock.unlock();
}

一個條件變量須要與一把鎖關聯,線程在開始等待條件以前必須得到鎖。獲取鎖後,線程檢查等待的條件是否爲真。

若是爲真,線程將繼續執行並解鎖;
若是不爲真,線程會調用 await(),它將原子的解鎖並阻塞等待條件。
當另外一個線程調用 signal() 或 signalAll(),意味着對應的條件可能變爲真, await() 將原子的恢復運行並從新加鎖。

條件變量解決哲學家就餐問題:

public class Philosopher4 extends Thread {

private boolean eating;
private Philosopher4 left;
private Philosopher4 right;
private ReentrantLock table;
private Condition condition;
private Random random;

public Philosopher4(ReentrantLock table) {
this.eating = false;
this.table = table;
this.condition = table.newCondition();
this.random = new Random();
}

public void setLeft(Philosopher4 left) {
this.left = left;
}

public void setRight(Philosopher4 right) {
this.right = right;
}

@Override
public void run() {
try {
while (true) {
think();
eat();
}
} catch (InterruptedException e) {
// ...
}
}

private void think() throws InterruptedException {
this.table.lock();
try {
this.eating = false;
this.left.condition.signal();
this.right.condition.signal();
} finally {
table.unlock();
}
Thread.sleep(1000);
}

private void eat() throws InterruptedException {
this.table.lock();
try {
while (left.eating || right.eating) {
this.condition.await();
}
this.eating = true;
} finally {
this.table.unlock();
}
Thread.sleep(1000);
}
}

原子變量

原子變量是無鎖(lock-free) 非阻塞(non-blocking)算法的基礎,這種算法能夠不用鎖和阻塞來達到同步的目的。



15個Java多線程面試題及回答

1.如今有T一、T二、T3三個線程,你怎樣保證T2在T1執行完後執行,T3在T2執行完後執行?

這個線程問題一般會在第一輪或電話面試階段被問到,目的是檢測你對」join」方法是否熟悉。這個多線程問題比較簡單,能夠用join方法實現。

2.在Java中Lock接口比synchronized塊的優點是什麼?你須要實現一個高效的緩存,它容許多個用戶讀,但只容許一個用戶寫,以此來保持它的完整性,你會怎樣去實現它?

lock接口在多線程和併發編程中最大的優點是它們爲讀和寫分別提供了鎖,它能知足你寫像ConcurrentHashMap這樣的高性能數據結構和有條件的阻塞。Java線程面試的問題愈來愈會根據面試者的回答來提問。我強烈建議在你去參加多線程的面試以前認真讀一下Locks,由於當前其大量用於構建電子交易終統的客戶端緩存和交易鏈接空間。

3.在java中wait和sleep方法的不一樣?

一般會在電話面試中常常被問到的Java線程面試問題。最大的不一樣是在等待時wait會釋放鎖,而sleep一直持有鎖。Wait一般被用於線程間交互,sleep一般被用於暫停執行。

4.用Java實現阻塞隊列。

這是一個相對艱難的多線程面試問題,它能達到不少的目的。第一,它能夠檢測侯選者是否能實際的用Java線程寫程序;第二,能夠檢測侯選者對併發場景的理解,而且你能夠根據這個問不少問題。若是他用wait()和notify()方法來實現阻塞隊列,你能夠要求他用最新的Java 5中的併發類來再寫一次。

5.用Java寫代碼來解決生產者——消費者問題。

與上面的問題很相似,但這個問題更經典,有些時候面試都會問下面的問題。在Java中怎麼解決生產者——消費者問題,固然有不少解決方法,我已經分享了一種用阻塞隊列實現的方法。有些時候他們甚至會問怎麼實現哲學家進餐問題。

6.用Java編程一個會致使死鎖的程序,你將怎麼解決?

這是我最喜歡的Java線程面試問題,由於即便死鎖問題在寫多線程併發程序時很是廣泛,可是不少侯選者並不能寫deadlock free code(無死鎖代碼?),他們很掙扎。只要告訴他們,你有N個資源和N個線程,而且你須要全部的資源來完成一個操做。爲了簡單這裏的n能夠替換爲2,越大的數據會使問題看起來更復雜。經過避免Java中的死鎖來獲得關於死鎖的更多信息。

7.什麼是原子操做,Java中的原子操做是什麼?

很是簡單的java線程面試問題,接下來的問題是你須要同步一個原子操做。

8.Java中的volatile關鍵是什麼做用?怎樣使用它?在Java中它跟synchronized方法有什麼不一樣?

自從Java 5和Java內存模型改變之後,基於volatile關鍵字的線程問題愈來愈流行。應該準備好回答關於volatile變量怎樣在併發環境中確保可見性、順序性和一致性。

9.什麼是競爭條件?你怎樣發現和解決競爭?

這是一道出如今多線程面試的高級階段的問題。大多數的面試官會問最近你遇到的競爭條件,以及你是怎麼解決的。有些時間他們會寫簡單的代碼,而後讓你檢測出代碼的競爭條件。能夠參考我以前發佈的關於Java競爭條件的文章。在我看來這是最好的java線程面試問題之一,它能夠確切的檢測候選者解決競爭條件的經驗,or writing code which is free of data race or any other race condition。關於這方面最好的書是《Concurrency practices in Java》。

10.你將如何使用thread dump?你將如何分析Thread dump?

在UNIX中你能夠使用kill -3,而後thread dump將會打印日誌,在windows中你能夠使用」CTRL+Break」。很是簡單和專業的線程面試問題,可是若是他問你怎樣分析它,就會很棘手。

11.爲何咱們調用start()方法時會執行run()方法,爲何咱們不能直接調用run()方法?

這是另外一個很是經典的java多線程面試問題。這也是我剛開始寫線程程序時候的困惑。如今這個問題一般在電話面試或者是在初中級Java面試的第一輪被問到。這個問題的回答應該是這樣的,當你調用start()方法時你將建立新的線程,而且執行在run()方法裏的代碼。可是若是你直接調用run()方法,它不會建立新的線程也不會執行調用線程的代碼。閱讀我以前寫的《start與run方法的區別》這篇文章來得到更多信息。

12.Java中你怎樣喚醒一個阻塞的線程?

這是個關於線程和阻塞的棘手的問題,它有不少解決方法。若是線程遇到了IO阻塞,我而且不認爲有一種方法能夠停止線程。若是線程由於調用wait()、sleep()、或者join()方法而致使的阻塞,你能夠中斷線程,而且經過拋出InterruptedException來喚醒它。我以前寫的《How to deal with blocking methods in java》有不少關於處理線程阻塞的信息。

13)在Java中CycliBarriar和CountdownLatch有什麼區別?

這個線程問題主要用來檢測你是否熟悉JDK5中的併發包。這兩個的區別是CyclicBarrier能夠重複使用已經經過的障礙,而CountdownLatch不能重複使用。

14. 什麼是不可變對象,它對寫併發應用有什麼幫助?

另外一個多線程經典面試問題,並不直接跟線程有關,但間接幫助不少。這個java面試問題能夠變的很是棘手,若是他要求你寫一個不可變對象,或者問你爲何String是不可變的。

15.你在多線程環境中遇到的共同的問題是什麼?你是怎麼解決它的?

多線程和併發程序中常遇到的有Memory-interface、競爭條件、死鎖、活鎖和飢餓。問題是沒有止境的,若是你弄錯了,將很難發現和調試。這是大多數基於面試的,而不是基於實際應用的Java線程問題。


60道最多見的Java多線程面試題


  1. 多線程有什麼用?

  2. 線程和進程的區別是什麼?

  3. ava實現線程有哪幾種方式?

  4. 啓動線程方法start()和run()有什麼區別?

  5. 怎麼終止一個線程?如何優雅地終止線程?

  6. 一個線程的生命週期有哪幾種狀態?它們之間如何流轉的?

  7. 線程中的wait()和sleep()方法有什麼區別?

  8. 多線程同步有哪幾種方法?

  9. 什麼是死鎖?如何避免死鎖?

  10. 多線程之間如何進行通訊?

  11. 線程怎樣拿到返回結果?

  12. violatile關鍵字的做用?

  13. 新建T一、T二、T3三個線程,如何保證它們按順序執行?

  14. 怎麼控制同一時間只有3個線程運行?

  15. 爲何要使用線程池?

  16. 經常使用的幾種線程池並講講其中的工做原理。

  17. 線程池啓動線程submit()和execute()方法有什麼不一樣?

  18. CyclicBarrier和CountDownLatch的區別?

  19. 什麼是活鎖、飢餓、無鎖、死鎖?

  20. 什麼是原子性、可見性、有序性?

  21. 什麼是守護線程?有什麼用?

  22. 怎麼中斷一個線程?如何保證中斷業務不影響?

  23. 一個線程運行時發生異常會怎樣?

  24. 什麼是重入鎖?

  25. Synchronized有哪幾種用法?

  26. Fork/Join框架是幹什麼的?

  27. 線程數過多會形成什麼異常?

  28. 說說線程安全的和不安全的集合。

  29. 什麼是CAS算法?在多線程中有哪些應用。

  30. 怎麼檢測一個線程是否擁有鎖?

  31. Jdk中排查多線程問題用什麼命令?

  32. 線程同步須要注意什麼?

  33. 線程wait()方法使用有什麼前提?

  34. Fork/Join框架使用有哪些要注意的地方?

  35. 線程之間如何傳遞數據?

  36. 保證"可見性"有哪幾種方式?

  37. 說幾個經常使用的Lock接口實現鎖。

  38. ThreadLocal是什麼?有什麼應用場景?

  39. ReadWriteLock有什麼用?

  40. FutureTask是什麼?

  41. 怎麼喚醒一個阻塞的線程?

  42. 不可變對象對多線程有什麼幫助?

  43. 多線程上下文切換是什麼意思?

  44. Java中用到了什麼線程調度算法?

  45. Thread.sleep(0)的做用是什麼?

  46. Java內存模型是什麼,哪些區域是線程共享的,哪些是不共享的

  47. 什麼是樂觀鎖和悲觀鎖?

  48. Hashtable的size()方法爲何要作同步?

  49. 同步方法和同步塊,哪一種更好?

  50. 什麼是自旋鎖?

  51. Runnable和Thread用哪一個好?

  52. Java中notify和notifyAll有什麼區別?

  53. 爲何wait/notify/notifyAll這些方法不在thread類裏面?

  54. 爲何wait和notify方法要在同步塊中調用?

  55. 爲何你應該在循環中檢查等待條件?

  56. Java中堆和棧有什麼不一樣?

  57. 你如何在Java中獲取線程堆棧?

  58. 如何建立線程安全的單例模式?

  59. 什麼是阻塞式方法?

  60. 提交任務時線程池隊列已滿會時發會生什麼?

本文分享自微信公衆號 - JAVA高級架構(gaojijiagou)。
若有侵權,請聯繫 support@oschina.cn 刪除。
本文參與「OSC源創計劃」,歡迎正在閱讀的你也加入,一塊兒分享。

相關文章
相關標籤/搜索