很多核心Java面試題來源於多執行緒(Multi-Threading)和集合框架(Collections Framework),理解核心執行緒概念時,嫻熟的實際經驗是必需的。這篇文章收集了Java執行緒方面一些典型的問題,這些問題經常被高階工程師所問到。
0、Java中多執行緒同步是什麼?
在多執行緒程式下,同步能控制對共享資源的訪問。如果沒有同步,當一個Java執行緒在修改一個共享變數時,另外一個執行緒正在使用或者更新同一個變數,這樣容易導致程式出現錯誤的結果。
1、解釋實現多執行緒的幾種方法?
一Java執行緒可以實現Runnable介面或者繼承Thread類來實現,當你打算多重繼承時,優先選擇實現Runnable。
2、Thread.start()與Thread.run()有什麼區別?
Thread.start()方法(native)啟動執行緒,使之進入就緒狀態,當cpu分配時間該執行緒時,由JVM排程執行run()方法。
3、為什麼需要run()和start()方法,我們可以只用run()方法來完成任務嗎?
我們需要run()&start()這兩個方法是因為JVM建立一個單獨的執行緒不同於普通方法的呼叫,所以這項工作由執行緒的start方法來完成,start由本地方法實現,需要顯示地被呼叫,使用這倆個方法的另外一個好處是任何一個物件都可以作為執行緒執行,只要實現了Runnable介面,這就避免因繼承了Thread類而造成的Java的多繼承問題。
4、什麼是ThreadLocal類,怎麼使用它?
ThreadLocal是一個執行緒級別的區域性變數,並非“本地執行緒”。ThreadLocal為每個使用該變數的執行緒提供了一個獨立的變數副本,每個執行緒修改副本時不影響其它執行緒物件的副本(譯者注)。
下面是執行緒區域性變數(ThreadLocal variables)的關鍵點:
一個執行緒區域性變數(ThreadLocal variables)為每個執行緒方便地提供了一個單獨的變數。
ThreadLocal例項通常作為靜態的私有的(private static)欄位出現在一個類中,這個類用來關聯一個執行緒。
當多個執行緒訪問ThreadLocal例項時,每個執行緒維護ThreadLocal提供的獨立的變數副本。
常用的使用可在DAO模式中見到,當DAO類作為一個單例類時,資料庫連結(connection)被每一個執行緒獨立的維護,互不影響。(基於執行緒的單例)
ThreadLocal難於理解,下面這些引用連線有助於你更好的理解它。
《Good article on ThreadLocal on IBM DeveloperWorks 》、《理解ThreadLocal》、《Managing data : Good example》、《Refer Java API Docs》
5、什麼時候丟擲InvalidMonitorStateException異常,為什麼?
呼叫wait()/notify()/notifyAll()中的任何一個方法時,如果當前執行緒沒有獲得該物件的鎖,那麼就會丟擲IllegalMonitorStateException的異常(也就是說程式在沒有執行物件的任何同步塊或者同步方法時,仍然嘗試呼叫wait()/notify()/notifyAll()時)。由於該異常是RuntimeExcpetion的子類,所以該異常不一定要捕獲(儘管你可以捕獲只要你願意).作為RuntimeException,此類異常不會在wait(),notify(),notifyAll()的方法簽名提及。
6、Sleep()、suspend()和wait()之間有什麼區別?
Thread.sleep()使當前執行緒在指定的時間處於“非執行”(Not Runnable)狀態。執行緒一直持有物件的監視器。比如一個執行緒當前在一個同步塊或同步方法中,其它執行緒不能進入該塊或方法中。如果另一執行緒呼叫了interrupt()方法,它將喚醒那個“睡眠的”執行緒。
注意:sleep()是一個靜態方法。這意味著只對當前執行緒有效,一個常見的錯誤是呼叫t.sleep(),(這裡的t是一個不同於當前執行緒的執行緒)。即便是執行t.sleep(),也是當前執行緒進入睡眠,而不是t執行緒。t.suspend()是過時的方法,使用suspend()導致執行緒進入停滯狀態,該執行緒會一直持有物件的監視器,suspend()容易引起死鎖問題。
object.wait()使當前執行緒出於“不可執行”狀態,和sleep()不同的是wait是object的方法而不是thread。呼叫object.wait()時,執行緒先要獲取這個物件的物件鎖,當前執行緒必須在鎖物件保持同步,把當前執行緒新增到等待佇列中,隨後另一執行緒可以同步同一個物件鎖來呼叫object.notify(),這樣將喚醒原來等待中的執行緒,然後釋放該鎖。基本上wait()/notify()與sleep()/interrupt()類似,只是前者需要獲取物件鎖。
7、在靜態方法上使用同步時會發生什麼事?
同步靜態方法時會獲取該類的“Class”物件,所以當一個執行緒進入同步的靜態方法中時,執行緒監視器獲取類本身的物件鎖,其它執行緒不能進入這個類的任何靜態同步方法。它不像例項方法,因為多個執行緒可以同時訪問不同例項同步例項方法。
8、當一個同步方法已經執行,執行緒能夠呼叫物件上的非同步例項方法嗎?
可以,一個非同步方法總是可以被呼叫而不會有任何問題。實際上,Java沒有為非同步方法做任何檢查,鎖物件僅僅在同步方法或者同步程式碼塊中檢查。如果一個方法沒有宣告為同步,即使你在使用共享資料Java照樣會呼叫,而不會做檢查是否安全,所以在這種情況下要特別小心。一個方法是否宣告為同步取決於臨界區訪問(critial section access),如果方法不訪問臨界區(共享資源或者資料結構)就沒必要宣告為同步的。
下面有一個示例說明:Common類有兩個方法synchronizedMethod1()和method1(),MyThread類在獨立的執行緒中呼叫這兩個方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public class Common { public synchronized void synchronizedMethod1() { System.out.println("synchronizedMethod1 called"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("synchronizedMethod1 done"); } public void method1() { System.out.println("Method 1 called"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("Method 1 done"); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
public class MyThread extends Thread { private int id = 0; private Common common; public MyThread(String name, int no, Common object) { super(name); common = object; id = no; } public void run() { System.out.println("Running Thread" + this.getName()); try { if (id == 0) { common.synchronizedMethod1(); } else { common.method1(); } } catch (Exception e) { e.printStackTrace(); } } public static void main(String[] args) { Common c = new Common(); MyThread t1 = new MyThread("MyThread-1", 0, c); MyThread t2 = new MyThread("MyThread-2", 1, c); t1.start(); t2.start(); } } |
1 |
這裡是程式的輸出:
1 2 3 4 5 6 |
Running ThreadMyThread-1 synchronizedMethod1 called Running ThreadMyThread-2 Method 1 called synchronizedMethod1 done Method 1 done |
結果表明即使synchronizedMethod1()方法執行了,method1()也會被呼叫。
9、 在一個物件上兩個執行緒可以呼叫兩個不同的同步例項方法嗎?
不能,因為一個物件已經同步了例項方法,執行緒獲取了物件的物件鎖。所以只有執行完該方法釋放物件鎖後才能執行其它同步方法。看下面程式碼示例非常清晰:Common 類 有synchronizedMethod1()和synchronizedMethod2()方法,MyThread呼叫這兩個方法。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
public class Common { public synchronized void synchronizedMethod1() { System.out.println("synchronizedMethod1 called"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("synchronizedMethod1 done"); } public synchronized void synchronizedMethod2() { System.out.println("synchronizedMethod2 called"); try { Thread.sleep(1000); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("synchronizedMethod2 done"); } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
public class MyThread extends Thread { private int id = 0; private Common common; public MyThread(String name, int no, Common object) { super(name); common = object; id = no; } public void run() { System.out.println("Running Thread" + this.getName()); try { if (id == 0) { common.synchronizedMethod1(); } else { common.synchronizedMethod2(); } } catch (Exception e) { e.printStackTrace(); } } public static void main(String[] args) { Common c = new Common(); MyThread t1 = new MyThread("MyThread-1", 0, c); MyThread t2 = new MyThread("MyThread-2", 1, c); t1.start(); t2.start(); } } |
10、 什麼是死鎖
死鎖就是兩個或兩個以上的執行緒被無限的阻塞,執行緒之間相互等待所需資源。這種情況可能發生在當兩個執行緒嘗試獲取其它資源的鎖,而每個執行緒又陷入無限等待其它資源鎖的釋放,除非一個使用者程式被終止。就JavaAPI而言,執行緒死鎖可能發生在一下情況。
●當兩個執行緒相互呼叫Thread.join()
●當兩個執行緒使用巢狀的同步塊,一個執行緒佔用了另外一個執行緒必需的鎖,互相等待時被阻塞就有可能出現死鎖。
11、什麼是執行緒餓死,什麼是活鎖?
執行緒餓死和活鎖雖然不想是死鎖一樣的常見問題,但是對於併發程式設計的設計者來說就像一次邂逅一樣。
當所有執行緒阻塞,或者由於需要的資源無效而不能處理,不存在非阻塞執行緒使資源可用。JavaAPI中執行緒活鎖可能發生在以下情形:
●當所有執行緒在程式中執行Object.wait(0),引數為0的wait方法。程式將發生活鎖直到在相應的物件上有執行緒呼叫Object.notify()或者Object.notifyAll()。
●當所有執行緒卡在無限迴圈中。
這裡的問題並不詳盡,我相信還有很多重要的問題並未提及,您認為還有哪些問題應該包括在上面呢?歡迎在評論中分享任何形式的問題與建議。
原文:Sachin FromDev 編譯:伯樂線上 –劉志軍
【如需轉載,請標註並保留原文連結、譯文連結和譯者等資訊,謝謝合作!】