1. 程式人生 > >Java面試題——多執行緒高併發

Java面試題——多執行緒高併發

  1. 什麼是執行緒?
    執行緒是程序中的一個實體,是被系統獨立排程和分派的基本單位,它被包含在程序之中,是程序中的實際運作單位。執行緒自己不擁有系統資源,只擁有一點兒在執行中必不可少的資源,但它可與同屬一個程序的其它執行緒共享程序所擁有的全部資源。一個執行緒可以建立和撤消另一個執行緒,同一程序中的多個執行緒之間可以併發執行。執行緒也有就緒、阻塞和執行三種基本狀態。我們通過多執行緒程式設計,能更高效的提高系統內多個程式間併發執行的程度,從而顯著提高系統資源的利用率和吞吐量。

  2. 執行緒和程序有什麼區別?
    i):在引入執行緒的作業系統中,通常都是把程序作為分配資源的基本單位,也是搶佔處理機的排程單位,而把執行緒作為獨立執行和獨立排程的基本單位。在多執行緒OS中,程序不是一個可執行的實體。
    ii):對於地址空間和其它資源(如開啟檔案):程序間相互獨立,同一程序的各執行緒間共享。某程序內的執行緒在其它程序不可見。執行緒是程序的子集,一個程序可以有很多執行緒。
    iii):通訊:不同程序之間通過IPC(程序間通訊)介面進行通訊。同一程序的執行緒間可以直接讀寫程序資料段(如全域性變數)來進行通訊——需要程序同步和互斥手段的輔助,以保證資料的一致性。
    iv):排程和切換:執行緒上下文切換比程序上下文切換要快得多。

  3. Java中有哪些方式可以實現執行緒?哪種更好?
    在語言層面有兩種方式。可以繼承java.lang.Thread執行緒類,但是它需要呼叫java.lang.Runnable介面來執行。由於執行緒類本身就是呼叫的Runnable介面,所以你可以繼承java.lang.Thread 類或者直接呼叫Runnable介面來重寫run()方法實現執行緒。
    如果是想實現多繼承,則呼叫Runnable介面。

  4. sleep()和wait()有什麼區別?
    sleep是執行緒類(Thread)的方法,導致此執行緒暫停執行指定時間,給執行機會給其他執行緒,但是監控狀態依然保持,到時後會自動恢復。呼叫sleep不會釋放物件鎖。
    wait是Object類的方法,對此物件呼叫wait方法導致本執行緒放棄物件鎖,進入等待此物件的等待鎖定池,只有針對此物件發出notify方法(或notifyAll)後本執行緒才進入物件鎖定池準備獲得物件鎖進入執行狀態。
    see more:

    http://blog.csdn.net/HaixWang/article/details/79376181

sleep是執行緒類(Thread)的方法,導致此執行緒暫停執行指定時間,給執行機會給其他執行緒,但是監控狀態依然保持,到時後會自動恢復。呼叫sleep不會釋放物件鎖。將執行機會(CPU)讓給其他執行緒,但是物件的鎖依然保持,因此休眠時間結束後會自動恢復。

wait是Object類的方法,對此物件呼叫wait方法導致本執行緒放棄物件鎖,進入等待此物件的等待鎖定池,只有針對此物件發出notify方法(或notifyAll)後本執行緒才進入物件鎖定池準備獲得物件鎖進入執行狀態。如果執行緒重新獲得物件的鎖就可以進入就緒狀態(還是等待)
see more:

http://blog.csdn.net/HaixWang/article/details/79376181

在JDK1.2以前的版本如果要實現執行緒的暫停、恢復和停止的方法分別是suspend()、resume()、stop()。但是從JDK1.2以後這些方法已經被遺棄

共同點:都丟擲InterruptedException異常

yield方法會臨時暫停當前正在執行的執行緒,來讓有同樣優先順序的正在等待的執行緒有機會執行。**如果沒有正在等待的執行緒,或者所有正在等待的執行緒的優先順序都比較低,那麼該執行緒會繼續執行。**執行了yield方法的執行緒什麼時候會繼續執行由執行緒排程器來決定,不同的廠商可能有不同的行為。yield方法不保證當前的執行緒會暫停或者停止,(也就是說不保證時效),但是可以保證當前執行緒在呼叫yield方法時會放棄CPU。

  1. 為什麼需要並行設計?
    業務需求:業務上需要多個邏輯單元,比如多個客戶端要傳送請求
    效能需求:在多核OS中,使用多執行緒併發執行效能會比單執行緒執行的效能好很多

  2. 併發和並行的區別:
    解釋一:並行是指兩個或者多個執行緒在同一時刻發生;而併發是指兩個或多個執行緒在同一時間間隔發生(交替執行)。
    解釋二:並行是在不同實體上的多個事件(多個JVM),併發是在同一實體上的多個事件(一個JVM)。
    並行又分在一臺處理器上同時處理多個任務,在多臺處理器上同時處理多個任務。如hadoop分散式叢集

  3. 程序死鎖的四個必要條件以及解除死鎖的基本策略:
    (1)互斥條件:執行緒對資源的訪問是排他性的,如果一個執行緒對佔用了某資源,那麼其他執行緒必須處於等待狀態,直到資源被釋放。
    (2)請求和保持條件:執行緒T1至少已經保持了一個資源R1佔用,但又提出對另一個資源R2請求,而此時,資源R2被其他執行緒T2佔用,於是該執行緒T1也必須等待,但又對自己保持的資源R1不釋放。
    (3)不可剝奪條件:是指程序已獲得的資源,在未完成使用之前,不可被剝奪,只能在使用完後自己釋放
    (4)環路等待條件:在死鎖發生時,必然存在一個“程序-資源環形鏈”。
    1.預防死鎖:通過設定一些限制條件,去破壞產生死鎖的必要條件
    2.避免死鎖:在資源分配過程中,使用某種方法避免系統進入不安全的狀態,從而避免發生死鎖
    3.檢測死鎖:允許死鎖的發生,但是通過系統的檢測之後,採取一些措施,將死鎖清除掉
    4.解除死鎖:該方法與檢測死鎖配合使用

  4. 解釋一下活鎖:
    是指執行緒1可以使用資源,但它很禮貌,讓其他執行緒先使用資源,執行緒2也可以使用資源,但它很紳士,也讓其他執行緒先使用資源。釋放完後,雙方發現資源滿足需求了,又都去強佔資源,但是又只拿到一部分,就這樣,資源在各個執行緒間一直往復。這樣你讓我,我讓你,最後兩個執行緒都無法使用資源。

  5. Thread 類中的start() 和 run() 方法有什麼區別?
    i):首先,start方法內部會呼叫run方法。
    ii):start與run方法的主要區別在於當程式呼叫start方法一個新執行緒將會被建立,並且在run方法中的程式碼將會在新執行緒上執行,然而在你直接呼叫run方法的時候,程式並不會建立新執行緒,run方法內部的程式碼將在當前執行緒上執行。大多數情況下呼叫run方法是一個bug或者變成失誤。因為呼叫者的初衷是呼叫start方法去開啟一個新的執行緒,這個錯誤可以被很多靜態程式碼覆蓋工具檢測出來,比如與fingbugs. 如果你想要執行需要消耗大量時間的任務,你最好使用start方法,否則在你呼叫run方法的時候,你的主執行緒將會被卡住。
    iii):還有一個區別在於,一但一個執行緒被啟動,你不能重複呼叫該thread物件的start方法,呼叫已經啟動執行緒的start方法將會報IllegalStateException異常, 而你卻可以重複呼叫run方法。

  6. 概括的解釋下執行緒的幾種可用狀態。
    答:
    執行緒在執行過程中,可以處於下面幾種狀態:

  • 建立(New):新建立了一個執行緒物件。
  • 就緒(Runnable): 執行緒物件建立後,其他執行緒(比如main執行緒)呼叫了該物件的start()方法。該狀態的執行緒位於可執行執行緒池中,等待被執行緒排程選中,獲取cpu 的使用權(或者說cpu時間片)
  • 執行(Running):可執行狀態(runnable)的執行緒獲得了cpu 時間片(timeslice) ,執行程式程式碼
  • 阻塞(Waiting): 阻塞狀態是指執行緒因為某種原因放棄了cpu 使用權,也即讓出了cpu timeslice,暫時停止執行。阻塞的情況分三種:
    (一). 等待阻塞:執行(running)的執行緒執行o.wait()方法,JVM會把該執行緒放入等待佇列(waitting queue)中。
    (二). 同步阻塞:執行(running)的執行緒在獲取物件的同步鎖時,若該同步鎖被別的執行緒佔用,則JVM會把該執行緒放入鎖池(lock pool)中。
    (三). 其他阻塞:執行(running)的執行緒執行Thread.sleep(long ms)[監控狀態依然保持,到時後會自動恢復]或t.join()方法,或者發出了I/O請求時,JVM會把該執行緒置為阻塞狀態。當sleep()狀態超時、join()等待執行緒終止或者超時、或者I/O處理完畢時,執行緒重新轉入可執行(runnable)狀態.阻塞狀態又有多種情況,可能是因為呼叫wait()方法進入等待池,也可能是執行同步方法或同步程式碼塊進入等鎖池,或者是呼叫了sleep()方法或join()方法等待休眠或其他執行緒結束,或是因為發生了I/O中斷。
  • 死亡(Dead):執行緒完成了執行,或者被強制結束執行。執行緒run()、main() 方法執行結束,或者因異常退出了run()方法,則該執行緒結束生命週期。死亡的執行緒不可再次復生。在一個死去的執行緒上呼叫start()方法,會丟擲java.lang.IllegalThreadStateException異常。
    **補充:**可執行狀態
    呼叫執行緒的start()方法,此執行緒進入可執行狀態。
    當前執行緒sleep()方法結束,其他執行緒join()結束,等待使用者輸入完畢,某個執行緒拿到物件鎖,這些執行緒也將進入可執行狀態。
    當前執行緒時間片用完了,呼叫當前執行緒的yield()方法,當前執行緒進入可執行狀態。
    鎖池裡的執行緒拿到物件鎖後,進入可執行狀態。
  1. Java記憶體模型是什麼?
    Java記憶體模型規定和指引Java程式在不同的記憶體架構、CPU和作業系統間有確定性地行為。它在多執行緒的情況下尤其重要。Java記憶體模型對一個執行緒所做的變動能被其它執行緒可見提供了保證,它們之間是先行發生關係。這個關係定義了一些規則讓程式設計師在併發程式設計時思路更清晰。比如,先行發生關係確保了:
    執行緒內的程式碼能夠按先後順序執行,這被稱為程式次序規則。
    對於同一個鎖,一個解鎖操作一定要發生在時間上後發生的另一個鎖定操作之前,也叫做管程鎖定規則。
    前一個對volatile的寫操作在後一個volatile的讀操作之前,也叫volatile變數規則。
    一個執行緒內的任何操作必需在這個執行緒的start()呼叫之後,也叫作執行緒啟動規則。
    一個執行緒的所有操作都會線上程終止之前,執行緒終止規則。
    一個物件的終結操作必需在這個物件構造完成之後,也叫物件終結規則。
    可傳遞性
    volatile不具備原子性,有效阻止指令重排序。

  2. Java中的volatile 變數是什麼?
    volatile是一個特殊的修飾符,只有成員變數才能使用它。在Java併發程式缺少同步類的情況下,多執行緒對成員變數的操作對其它執行緒是透明的。volatile變數可以保證下一個讀取操作會在前一個寫操作之後發生,就是上一題的volatile變數規則。檢視更多volatile的相關內容。(http://javarevisited.blogspot.com/2011/06/volatile-keyword-java-example-tutorial.html)

  3. Java執行緒池
    Java通過Executors提供四種執行緒池,分別為:

  • newCachedThreadPool建立一個可快取執行緒池,如果執行緒池長度超過處理需要,可靈活回收空閒執行緒,若無可回收,則新建執行緒。執行緒池為無限大,當執行第二個任務時第一個任務已經完成,會複用執行第一個任務的執行緒,而不用每次新建執行緒。
  • newFixedThreadPool 建立一個指定大小的執行緒池,可控制執行緒最大併發數,超出的執行緒會在佇列中等待。
  • newScheduledThreadPool 建立一個定長執行緒池,支援定時及週期性任務執行。
  • newSingleThreadExecutor 建立一個單執行緒化的執行緒池,它只會用唯一的工作執行緒來執行任務,保證所有任務按照指定順序(FIFO, LIFO, 優先順序)執行。
  1. 執行緒的共享資源和私有資源
    執行緒共享的環境包括:程序程式碼段、程序的公有資料(利用這些共享的資料,執行緒很容易的實現相互之間的通訊)、程序開啟的檔案描述符、訊號的處理器、程序的當前目錄和程序使用者ID與程序組ID。

執行緒擁有這許多共性的同時,還擁有自己的個性。有了這些個性,執行緒才能實現併發性。這些個性包括:

1.執行緒ID
  每個執行緒都有自己的執行緒ID,這個ID在本程序中是唯一的。程序用此來標識執行緒。

2.暫存器組的值
   由於執行緒間是併發執行的,每個執行緒有自己不同的執行線索,當從一個執行緒切換到另一個執行緒上時,必須將原有的執行緒的暫存器集合的狀態儲存,以便將來該執行緒在被重新切換到時能得以恢復。

3.執行緒的堆疊
   堆疊是保證執行緒獨立執行所必須的。
   執行緒函式可以呼叫函式,而被呼叫函式中又是可以層層巢狀的,所以執行緒必須擁有自己的函式堆疊,使得函式呼叫可以正常執行,不受其他執行緒的影響。

4.錯誤返回碼
   由於同一個程序中有很多個執行緒在同時執行,可能某個執行緒進行系統呼叫後設置了errno值,而在該執行緒還沒有處理這個錯誤,另外一個執行緒就在此時被排程器投入執行,這樣錯誤值就有可能被修改。所以,不同的執行緒應該擁有自己的錯誤返回碼變數。

5.執行緒的訊號遮蔽碼
   由於每個執行緒所感興趣的訊號不同,所以執行緒的訊號遮蔽碼應該由執行緒自己管理。但所有的執行緒都共享同樣的訊號處理器。

6.執行緒的優先順序
   由於執行緒需要像程序那樣能夠被排程,那麼就必須要有可供排程使用的引數,這個引數就是執行緒的優先順序。
  1. 自旋鎖

互斥同步的開銷都很大,並且,在大部分場景中,共享資料的鎖定狀態只會持續很短的一段時間。自旋鎖的思想是讓一個執行緒在請求一個共享資料的鎖時**執行忙迴圈(自旋)**一段時間,如果在這段時間內能獲得鎖,就可以避免進入阻塞狀態。

自選鎖雖然通過避免進入阻塞狀態能減少開銷,但是它需要進行忙迴圈操作佔用 CPU 時間,所以它只適用於共享資料的鎖定狀態很短的場景。

在 JDK 1.6 中引入了自適應的自旋鎖(自旋的時間不再固定),由前一次在同一個鎖上的自旋次數及鎖的擁有者的狀態來決定

  1. 怎麼檢測一個執行緒是否擁有鎖
    java.lang.Thread中有一個方法叫holdsLock(),它返回true如果當且僅當當前執行緒擁有某個具體物件的鎖。

  2. volatile 與 synchronized 的比較
    volatile主要用在多個執行緒感知例項變數被更改了場合,從而使得各個執行緒獲得最新的值。它強制執行緒每次從主記憶體中講到變數,而不是從執行緒的私有記憶體中讀取變數,從而保證了資料的可見性。
    關於synchronized,可參考:JAVA多執行緒之Synchronized關鍵字–物件鎖的特點
    比較:
    ①volatile輕量級,只能修飾變數。synchronized重量級,還可修飾方法
    ②volatile只能保證資料的可見性,不能用來同步,因為多個執行緒併發訪問volatile修飾的變數不會阻塞。
    synchronized不僅保證可見性,而且還保證原子性,因為,只有獲得了鎖的執行緒才能進入臨界區,從而保證臨界區中的所有語句都全部執行。多個執行緒爭搶synchronized鎖物件時,會出現阻塞。