您的位置:首頁>正文

Java多執行緒知識點(附代碼實例和常見面試題)

你臉上雲淡風輕, 又有誰明白你內心波濤洶湧

你腳下步履輕鬆, 又有誰知道你膝蓋藏著淤青

java多執行緒是面試中比被問到的問題, 也是程式設計的基礎。 這篇文章涵蓋了全方位3D剖析執行緒知識點。


應用程式Java對多執行緒的支援是非常強大的, 他遮罩掉了許多的技術細節, 讓我們可以輕鬆的開發多執行緒的。

Java裡面實現多執行緒三種方法

一、繼承 Thread類

二、實現 Runnable介面

注意:

鼓勵使用第二種方法, 因為Java裡面只允許單一繼承, 但允許實現多個介面。 第二個方法更加靈活。

run()並不是啟動執行緒, 而是簡單的方法調用。 只有start()是唯一的啟動方法。

並不是一啟動執行緒(調用start()方法)就執行這個執行緒, 而是進入就緒狀態, 什麼時候運行要看CUP。

三、使用ExecutorService、Callable、Future實現有返回結果的多執行緒

ExecutorService、Callable、Future這個物件實際上都是屬於Executor框架中的功能類。 想要詳細瞭解Executor框架的可以訪問。

ExecutoreService提供了submit()方法,

傳遞一個Callable, 或Runnable, 返回Future。 如果Executor後臺執行緒池還沒有完成Callable的計算, 這調用返回Future物件的get()方法, 會阻塞直到計算完成。

運行結果:

----程式開始運行----

>>>0 任務啟動

>>>0 任務終止

>>>0 任務返回運行結果,當前任務時間【1000毫秒】

>>>1 任務啟動

>>>1 任務終止

>>>1 任務返回運行結果,當前任務時間【1001毫秒】

>>>2 任務啟動

>>>2 任務終止

>>>2 任務返回運行結果,當前任務時間【1001毫秒】

>>>0 任務返回運行結果,當前任務時間【1000毫秒】

>>>1 任務返回運行結果,當前任務時間【1001毫秒】

>>>2 任務返回運行結果,當前任務時間【1001毫秒】

----程式結束運行----, 程式執行時間【3006毫秒】

執行緒的五種狀態:創建、就緒、運行、阻塞、終止。

(1)NEW(新建狀態:A thread that has not yet started is in this state.):產生實體Thread物件, 但沒有調用start()方法時的狀態, isAlive()的放回結果為false。

(2)RUNNABLE(可運行狀態:A thread executing in the Java virtual machine is in this state.):調用start()方法後的狀態。 處於RUNNABLE狀態的執行緒在JVM上是運行著的, 但是它可能還正在等待OS分配給它時間片才能運行, isAlive()的放回結果為true。

(3)BLOCKED(阻塞狀態:A thread that is blocked waiting for a monitor lock is in this state.):執行緒正在等待其它的執行緒釋放同步鎖, 以進入一個同步塊或者同步方法繼續運行;或者它已經進入了某個同步塊或同步方法, 在運行的過程中它調用了wait()方法,

正在等待重新返回這個同步塊或同步方法。

(4)WAITING(等候狀態:A thread that is waiting indefinitely for another thread to perform a particular action is in this state.):當前執行緒調用了java.lang.Object.wait()、java.lang.Thread.join()或者java.util.concurrent.locks.LockSupport.park()三個中的任意一個方法, 正在等待另外一個執行緒執行某個操作。 比如一個執行緒調用了某個物件的wait()方法, 正在等待其它執行緒調用這個物件的notify()或者notifyAll()方法來喚醒它;或者一個執行緒調用了另一個執行緒的join()方法, 正在等待這個方法運行結束。

(5)TIMED_WAITING(定時等候狀態:A thread that is waiting for another thread to perform an action for up to a specified waiting time is in this state.):當前執行緒調用了java.lang.Object.wait(long timeout)、java.lang.Thread.join(long millis)、java.util.concurrent.locks.LockSupport.packNanos(long nanos)、java.util.concurrent.locks.LockSupport.packUntil(long deadline)四個方法中的任意一個, 進入等候狀態, 但是與WAITING狀態不同的是, 它有一個最大等待時間, 即使等待的條件仍然沒有滿足, 只要到了這個時間它就會自動醒來。

(6)TERMINATED(死亡狀態:A thread that has exited is in this state.):run()方法運行結束或被未catch的InterruptedException等異常終結, 那麼該執行緒完成其歷史使命, 它的棧結構將解散,

也就是死亡了。 但是它仍然是一個Thread物件, 我們仍可以引用它, 就像其他物件一樣!它也不會被垃圾回收器回收了, 因為對該物件的引用仍然存在。 如此說來, 即使run()方法運行結束執行緒也沒有死啊!事實是, 一旦執行緒死去, 它就永遠不能重新啟動了, 也就是說, 不能再用start()方法讓它運行起來!如果強來的話會拋出IllegalThreadStateException異常。 如:t.start();

執行緒阻塞和恢復

wait() 方法

wait() 方法繼承自Object類(方法修飾符為fianl native, 這也解釋了為什麼condition類中不能重寫wait等方法)

阻塞:wait()方法的調用都會使當前執行緒阻塞。 該執行緒將會被放置到對該Object的請求等待佇列中, 然後讓出當前對Object所擁有的所有的同步請求。 執行緒會一直暫停所有執行緒調度, 直到下面其中一種情況發生:

① 其他執行緒調用了該Object的notify方法,而該執行緒剛好是那個被喚醒的執行緒;

② 其他執行緒調用了該Object的notifyAll方法;

③ 其他物件中斷/殺死了該執行緒;

④ (這種情況,只針對前兩個方法)執行緒在等待指定的時間後;

恢復:執行緒將會從等待佇列中移除,重新成為可調度執行緒。它會與其他執行緒以常規的方式競爭物件同步請求。一旦它重新獲得物件的同步請求,所有之前的請求狀態都會恢復,也就是執行緒調用wait的地方的狀態。執行緒將會在之前調用wait的地方繼續運行下去。

notify() 和notifyAll() 方法

notify的作用就是喚醒請求佇列中的一個執行緒,而notifyAll喚醒的是請求佇列中的所有執行緒。

被喚醒的執行緒不會馬上運行,除非獲取了該Object的鎖。也就是說,調用notify的執行緒,在調用notify後,不會像wait一樣,馬上阻塞執行緒的運行。而是繼續運行,直到相應的執行緒調度完成或者讓出Object的鎖。而被喚醒的執行緒會在當前執行緒讓出Object鎖後,與其他執行緒以常規的方式競爭物件鎖。

sleep() 方法:

sleep(…毫秒),指定以毫秒為單位的時間,使執行緒在該時間內進入執行緒阻塞狀態,期間得不到cpu的時間片,等到時間過去了,執行緒重新進入可執行狀態。(暫停執行緒,不會釋放鎖)

suspend() 和 resume() 方法:。

掛起和喚醒執行緒,suspend()使執行緒進入阻塞狀態,只有對應的resume()被調用的時候,執行緒才會進入可執行狀態。(不建議用,容易發生鎖死)

sleep() 方法:

sleep(…毫秒),指定以毫秒為單位的時間,使執行緒在該時間內進入執行緒阻塞狀態,期間得不到cpu的時間片,等到時間過去了,執行緒重新進入可執行狀態。(暫停執行緒,不會釋放鎖)

suspend() 和 resume() 方法

掛起和喚醒執行緒,suspend()使執行緒進入阻塞狀態,只有對應的resume()被調用的時候,執行緒才會進入可執行狀態。(不建議用,容易發生鎖死)

yield() 方法

會使的執行緒放棄當前分得的cpu時間片,但此時執行緒任然處於可執行狀態,隨時可以再次分得cpu時間片。yield()方法只能使同優先順序的執行緒有執行的機會。調用 yield() 的效果等價于調度程式認為該執行緒已執行了足夠的時間從而轉到另一個執行緒。(暫停當前正在執行的執行緒,並執行其他執行緒,且讓出的時間不可知)

join() 方法

也叫執行緒加入。是當前執行緒A調用另一個執行緒B的join()方法,當前執行緒轉A入阻塞狀態,直到執行緒B運行結束,執行緒A才由阻塞狀態轉為可執行狀態。

常見相關面試題

迅雷面試題,多執行緒交替列印ABC十次。

這道題看似思路簡單,其實主要需要用到wait()方法和notify()方法,還有關鍵字synchronized,只有充分理解了這些,才能解出這道題

輸出結果:ABABABABABABABABABAB

怎麼檢測一個執行緒是否擁有鎖?

電話面試被問到過,在java.lang.Thread中有一個方法叫holdsLock(),它返回true如果當且僅當當前執行緒擁有某個具體物件的鎖

Java中堆和棧有什麼不同?

為什麼把這個問題歸類在多執行緒和並發麵試題裡?因為棧是一塊和執行緒緊密相關的記憶體區域。每個執行緒都有自己的棧記憶體,用於存儲本地變數,方法參數和棧調用,一個執行緒中存儲的變數對其它執行緒是不可見的。而堆是所有執行緒共用的一片公用記憶體區域。物件都在堆裡創建,為了提升效率執行緒會從堆中弄一個緩存到自己的棧,如果多個執行緒使用該變數就可能引發問題,這時volatile 變數就可以發揮作用了,它要求執行緒從主存中讀取變數的值。

為什麼wait, notify 和 notifyAll這些方法不在thread類裡面?

一個很明顯的原因是JAVA提供的鎖是物件級的而不是執行緒級的,每個對象都有鎖,通過執行緒獲得。如果執行緒需要等待某些鎖那麼調用物件中的wait()方法就有意義了。如果wait()方法定義在Thread類中,執行緒正在等待的是哪個鎖就不明顯了。簡單的說,由於wait,notify和notifyAll都是鎖級別的操作,所以把他們定義在Object類中因為鎖屬於對象。

什麼是執行緒池? 為什麼要使用它?

創建執行緒要花費昂貴的資源和時間,如果任務來了才創建執行緒那麼回應時間會變長,而且一個進程能創建的執行緒數有限。為了避免這些問題,在程式啟動的時候就創建若干執行緒來回應處理,它們被稱為執行緒池,裡面的執行緒叫工作執行緒。從JDK1.5開始,Java API提供了Executor框架讓你可以創建不同的執行緒池。比如單執行緒池,每次處理一個任務;數目固定的執行緒池或者是緩存執行緒池(一個適合很多生存期短的任務的程式的可擴展執行緒池)

如何寫代碼來解決生產者消費者問題?

在現實中你解決的許多執行緒問題都屬於生產者消費者模型,就是一個執行緒生產任務供其它執行緒進行消費,你必須知道怎麼進行執行緒間通信來解決這個問題。比較低級的辦法是用wait和notify來解決這個問題,比較贊的辦法是用Semaphore 或者 BlockingQueue來實現生產者消費者模型

Java多執行緒中的鎖死

鎖死是指兩個或兩個以上的進程在執行過程中,因爭奪資源而造成的一種互相等待的現象,若無外力作用,它們都將無法推進下去。這是一個嚴重的問題,因為鎖死會讓你的程式掛起無法完成任務,鎖死的發生必須滿足以下四個條件:

互斥條件:一個資源每次只能被一個進程使用。

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

不剝奪條件:進程已獲得的資源,在末使用完之前,不能強行剝奪。

迴圈等待條件:若干進程之間形成一種頭尾相接的迴圈等待資源關係。

避免鎖死最簡單的方法就是阻止迴圈等待條件,將系統中所有的資源設置標誌位元、排序,規定所有的進程申請資源必須以一定的順序(昇冪或降冪)做操作來避免鎖死。

① 其他執行緒調用了該Object的notify方法,而該執行緒剛好是那個被喚醒的執行緒;

② 其他執行緒調用了該Object的notifyAll方法;

③ 其他物件中斷/殺死了該執行緒;

④ (這種情況,只針對前兩個方法)執行緒在等待指定的時間後;

恢復:執行緒將會從等待佇列中移除,重新成為可調度執行緒。它會與其他執行緒以常規的方式競爭物件同步請求。一旦它重新獲得物件的同步請求,所有之前的請求狀態都會恢復,也就是執行緒調用wait的地方的狀態。執行緒將會在之前調用wait的地方繼續運行下去。

notify() 和notifyAll() 方法

notify的作用就是喚醒請求佇列中的一個執行緒,而notifyAll喚醒的是請求佇列中的所有執行緒。

被喚醒的執行緒不會馬上運行,除非獲取了該Object的鎖。也就是說,調用notify的執行緒,在調用notify後,不會像wait一樣,馬上阻塞執行緒的運行。而是繼續運行,直到相應的執行緒調度完成或者讓出Object的鎖。而被喚醒的執行緒會在當前執行緒讓出Object鎖後,與其他執行緒以常規的方式競爭物件鎖。

sleep() 方法:

sleep(…毫秒),指定以毫秒為單位的時間,使執行緒在該時間內進入執行緒阻塞狀態,期間得不到cpu的時間片,等到時間過去了,執行緒重新進入可執行狀態。(暫停執行緒,不會釋放鎖)

suspend() 和 resume() 方法:。

掛起和喚醒執行緒,suspend()使執行緒進入阻塞狀態,只有對應的resume()被調用的時候,執行緒才會進入可執行狀態。(不建議用,容易發生鎖死)

sleep() 方法:

sleep(…毫秒),指定以毫秒為單位的時間,使執行緒在該時間內進入執行緒阻塞狀態,期間得不到cpu的時間片,等到時間過去了,執行緒重新進入可執行狀態。(暫停執行緒,不會釋放鎖)

suspend() 和 resume() 方法

掛起和喚醒執行緒,suspend()使執行緒進入阻塞狀態,只有對應的resume()被調用的時候,執行緒才會進入可執行狀態。(不建議用,容易發生鎖死)

yield() 方法

會使的執行緒放棄當前分得的cpu時間片,但此時執行緒任然處於可執行狀態,隨時可以再次分得cpu時間片。yield()方法只能使同優先順序的執行緒有執行的機會。調用 yield() 的效果等價于調度程式認為該執行緒已執行了足夠的時間從而轉到另一個執行緒。(暫停當前正在執行的執行緒,並執行其他執行緒,且讓出的時間不可知)

join() 方法

也叫執行緒加入。是當前執行緒A調用另一個執行緒B的join()方法,當前執行緒轉A入阻塞狀態,直到執行緒B運行結束,執行緒A才由阻塞狀態轉為可執行狀態。

常見相關面試題

迅雷面試題,多執行緒交替列印ABC十次。

這道題看似思路簡單,其實主要需要用到wait()方法和notify()方法,還有關鍵字synchronized,只有充分理解了這些,才能解出這道題

輸出結果:ABABABABABABABABABAB

怎麼檢測一個執行緒是否擁有鎖?

電話面試被問到過,在java.lang.Thread中有一個方法叫holdsLock(),它返回true如果當且僅當當前執行緒擁有某個具體物件的鎖

Java中堆和棧有什麼不同?

為什麼把這個問題歸類在多執行緒和並發麵試題裡?因為棧是一塊和執行緒緊密相關的記憶體區域。每個執行緒都有自己的棧記憶體,用於存儲本地變數,方法參數和棧調用,一個執行緒中存儲的變數對其它執行緒是不可見的。而堆是所有執行緒共用的一片公用記憶體區域。物件都在堆裡創建,為了提升效率執行緒會從堆中弄一個緩存到自己的棧,如果多個執行緒使用該變數就可能引發問題,這時volatile 變數就可以發揮作用了,它要求執行緒從主存中讀取變數的值。

為什麼wait, notify 和 notifyAll這些方法不在thread類裡面?

一個很明顯的原因是JAVA提供的鎖是物件級的而不是執行緒級的,每個對象都有鎖,通過執行緒獲得。如果執行緒需要等待某些鎖那麼調用物件中的wait()方法就有意義了。如果wait()方法定義在Thread類中,執行緒正在等待的是哪個鎖就不明顯了。簡單的說,由於wait,notify和notifyAll都是鎖級別的操作,所以把他們定義在Object類中因為鎖屬於對象。

什麼是執行緒池? 為什麼要使用它?

創建執行緒要花費昂貴的資源和時間,如果任務來了才創建執行緒那麼回應時間會變長,而且一個進程能創建的執行緒數有限。為了避免這些問題,在程式啟動的時候就創建若干執行緒來回應處理,它們被稱為執行緒池,裡面的執行緒叫工作執行緒。從JDK1.5開始,Java API提供了Executor框架讓你可以創建不同的執行緒池。比如單執行緒池,每次處理一個任務;數目固定的執行緒池或者是緩存執行緒池(一個適合很多生存期短的任務的程式的可擴展執行緒池)

如何寫代碼來解決生產者消費者問題?

在現實中你解決的許多執行緒問題都屬於生產者消費者模型,就是一個執行緒生產任務供其它執行緒進行消費,你必須知道怎麼進行執行緒間通信來解決這個問題。比較低級的辦法是用wait和notify來解決這個問題,比較贊的辦法是用Semaphore 或者 BlockingQueue來實現生產者消費者模型

Java多執行緒中的鎖死

鎖死是指兩個或兩個以上的進程在執行過程中,因爭奪資源而造成的一種互相等待的現象,若無外力作用,它們都將無法推進下去。這是一個嚴重的問題,因為鎖死會讓你的程式掛起無法完成任務,鎖死的發生必須滿足以下四個條件:

互斥條件:一個資源每次只能被一個進程使用。

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

不剝奪條件:進程已獲得的資源,在末使用完之前,不能強行剝奪。

迴圈等待條件:若干進程之間形成一種頭尾相接的迴圈等待資源關係。

避免鎖死最簡單的方法就是阻止迴圈等待條件,將系統中所有的資源設置標誌位元、排序,規定所有的進程申請資源必須以一定的順序(昇冪或降冪)做操作來避免鎖死。

同類文章
Next Article
喜欢就按个赞吧!!!
点击关闭提示