bat等大公司常考java多執行緒面試題

程式設計師歐陽思海發表於2019-03-04

1、說說程式,執行緒,協程之間的區別

簡而言之,程式是程式執行和資源分配的基本單位,一個程式至少有一個程式,一個程式至少有一個執行緒.程式在執行過程中擁有獨立的記憶體單元,而多個執行緒共享記憶體資源,減少切換次數,從而效率更高.執行緒是程式的一個實體,是cpu排程和分派的基本單位,是比程式更小的能獨立執行的基本單位.同一程式中的多個執行緒之間可以併發執行.

2、你瞭解守護執行緒嗎?它和非守護執行緒有什麼區別

程式執行完畢,jvm會等待非守護執行緒完成後關閉,但是jvm不會等待守護執行緒.守護執行緒最典型的例子就是GC執行緒

3、什麼是多執行緒上下文切換

多執行緒的上下文切換是指CPU控制權由一個已經正在執行的執行緒切換到另外一個就緒並等待獲取CPU執行權的執行緒的過程。

4、建立兩種執行緒的方式?他們有什麼區別?

通過實現java.lang.Runnable或者通過擴充套件java.lang.Thread類.相比擴充套件Thread,實現Runnable介面可能更優.原因有二:

  • Java不支援多繼承.因此擴充套件Thread類就代表這個子類不能擴充套件其他類.而實現Runnable介面的類還可能擴充套件另一個類.

  • 類可能只要求可執行即可,因此繼承整個Thread類的開銷過大.

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

start()方法被用來啟動新建立的執行緒,而且start()內部呼叫了run()方法,這和直接呼叫run()方法的效果不一樣。當你呼叫run()方法的時候,只會是在原來的執行緒中呼叫,沒有新的執行緒啟動,start()方法才會啟動新執行緒。

6、怎麼檢測一個執行緒是否持有物件監視器

Thread類提供了一個holdsLock(Object obj)方法,當且僅當物件obj的監視器被某條執行緒持有的時候才會返回true,注意這是一個static方法,這意味著”某條執行緒”指的是當前執行緒。

7、Runnable和Callable的區別

Runnable介面中的run()方法的返回值是void,它做的事情只是純粹地去執行run()方法中的程式碼而已;Callable介面中的call()方法是有返回值的,是一個泛型,和Future、FutureTask配合可以用來獲取非同步執行的結果。
這其實是很有用的一個特性,因為多執行緒相比單執行緒更難、更復雜的一個重要原因就是因為多執行緒充滿著未知性,某條執行緒是否執行了?某條執行緒執行了多久?某條執行緒執行的時候我們期望的資料是否已經賦值完畢?無法得知,我們能做的只是等待這條多執行緒的任務執行完畢而已。而Callable+Future/FutureTask卻可以方便獲取多執行緒執行的結果,可以在等待時間太長沒獲取到需要的資料的情況下取消該執行緒的任務

8、什麼導致執行緒阻塞

阻塞指的是暫停一個執行緒的執行以等待某個條件發生(如某資源就緒),學過作業系統的同學對它一定已經很熟悉了。Java 提供了大量方法來支援阻塞,下面讓我們逐一分析。

方法 說明
sleep() sleep() 允許 指定以毫秒為單位的一段時間作為引數,它使得執行緒在指定的時間內進入阻塞狀態,不能得到CPU 時間,指定的時間一過,執行緒重新進入可執行狀態。 典型地,sleep() 被用在等待某個資源就緒的情形:測試發現條件不滿足後,讓執行緒阻塞一段時間後重新測試,直到條件滿足為止
suspend() 和 resume() 兩個方法配套使用,suspend()使得執行緒進入阻塞狀態,並且不會自動恢復,必須其對應的resume() 被呼叫,才能使得執行緒重新進入可執行狀態。典型地,suspend() 和 resume() 被用在等待另一個執行緒產生的結果的情形:測試發現結果還沒有產生後,讓執行緒阻塞,另一個執行緒產生了結果後,呼叫 resume() 使其恢復。
yield() yield() 使當前執行緒放棄當前已經分得的CPU 時間,但不使當前執行緒阻塞,即執行緒仍處於可執行狀態,隨時可能再次分得 CPU 時間。呼叫 yield() 的效果等價於排程程式認為該執行緒已執行了足夠的時間從而轉到另一個執行緒
wait() 和 notify() 兩個方法配套使用,wait() 使得執行緒進入阻塞狀態,它有兩種形式,一種允許 指定以毫秒為單位的一段時間作為引數,另一種沒有引數,前者當對應的 notify() 被呼叫或者超出指定時間時執行緒重新進入可執行狀態,後者則必須對應的 notify() 被呼叫.

9、wait(),notify()和suspend(),resume()之間的區別

初看起來它們與 suspend() 和 resume() 方法對沒有什麼分別,但是事實上它們是截然不同的。區別的核心在於,前面敘述的所有方法,阻塞時都不會釋放佔用的鎖(如果佔用了的話),而這一對方法則相反。上述的核心區別導致了一系列的細節上的區別。

首先,前面敘述的所有方法都隸屬於 Thread 類,但是這一對卻直接隸屬於 Object 類,也就是說,所有物件都擁有這一對方法。初看起來這十分不可思議,但是實際上卻是很自然的,因為這一對方法阻塞時要釋放佔用的鎖,而鎖是任何物件都具有的,呼叫任意物件的 wait() 方法導致執行緒阻塞,並且該物件上的鎖被釋放。而呼叫 任意物件的notify()方法則導致從呼叫該物件的 wait() 方法而阻塞的執行緒中隨機選擇的一個解除阻塞(但要等到獲得鎖後才真正可執行)。

其次,前面敘述的所有方法都可在任何位置呼叫,但是這一對方法卻必須在 synchronized 方法或塊中呼叫,理由也很簡單,只有在synchronized 方法或塊中當前執行緒才佔有鎖,才有鎖可以釋放。同樣的道理,呼叫這一對方法的物件上的鎖必須為當前執行緒所擁有,這樣才有鎖可以釋放。因此,這一對方法呼叫必須放置在這樣的 synchronized 方法或塊中,該方法或塊的上鎖物件就是呼叫這一對方法的物件。若不滿足這一條件,則程式雖然仍能編譯,但在執行時會出現IllegalMonitorStateException 異常。

wait() 和 notify() 方法的上述特性決定了它們經常和synchronized關鍵字一起使用,將它們和作業系統程式間通訊機制作一個比較就會發現它們的相似性:synchronized方法或塊提供了類似於作業系統原語的功能,它們的執行不會受到多執行緒機制的干擾,而這一對方法則相當於 block 和wakeup 原語(這一對方法均宣告為 synchronized)。它們的結合使得我們可以實現作業系統上一系列精妙的程式間通訊的演算法(如訊號量演算法),並用於解決各種複雜的執行緒間通訊問題。

關於 wait() 和 notify() 方法最後再說明兩點:

第一:呼叫 notify() 方法導致解除阻塞的執行緒是從因呼叫該物件的 wait() 方法而阻塞的執行緒中隨機選取的,我們無法預料哪一個執行緒將會被選擇,所以程式設計時要特別小心,避免因這種不確定性而產生問題。

第二:除了 notify(),還有一個方法 notifyAll() 也可起到類似作用,唯一的區別在於,呼叫 notifyAll() 方法將把因呼叫該物件的 wait() 方法而阻塞的所有執行緒一次性全部解除阻塞。當然,只有獲得鎖的那一個執行緒才能進入可執行狀態。

談到阻塞,就不能不談一談死鎖,略一分析就能發現,suspend() 方法和不指定超時期限的 wait() 方法的呼叫都可能產生死鎖。遺憾的是,Java 並不在語言級別上支援死鎖的避免,我們在程式設計中必須小心地避免死鎖。

以上我們對 Java 中實現執行緒阻塞的各種方法作了一番分析,我們重點分析了 wait() 和 notify() 方法,因為它們的功能最強大,使用也最靈活,但是這也導致了它們的效率較低,較容易出錯。實際使用中我們應該靈活使用各種方法,以便更好地達到我們的目的。

11、產生死鎖的條件

1.互斥條件:一個資源每次只能被一個程式使用。
2.請求與保持條件:一個程式因請求資源而阻塞時,對已獲得的資源保持不放。
3.不剝奪條件:程式已獲得的資源,在末使用完之前,不能強行剝奪。
4.迴圈等待條件:若干程式之間形成一種頭尾相接的迴圈等待資源關係。

12、為什麼wait()方法和notify()/notifyAll()方法要在同步塊中被呼叫

這是JDK強制的,wait()方法和notify()/notifyAll()方法在呼叫前都必須先獲得物件的鎖
wait()方法和notify()/notifyAll()方法在放棄物件監視器時有什麼區別

wait()方法和notify()/notifyAll()方法在放棄物件監視器的時候的區別在於:wait()方法立即釋放物件監視器,notify()/notifyAll()方法則會等待執行緒剩餘程式碼執行完畢才會放棄物件監視器。

13、wait()與sleep()的區別

關於這兩者已經在上面進行詳細的說明,這裡就做個概括好了:

  • sleep()來自Thread類,和wait()來自Object類.呼叫sleep()方法的過程中,執行緒不會釋放物件鎖。而 呼叫 wait 方法執行緒會釋放物件鎖

  • sleep()睡眠後不出讓系統資源,wait讓其他執行緒可以佔用CPU

  • sleep(milliseconds)需要指定一個睡眠時間,時間一到會自動喚醒.而wait()需要配合notify()或者notifyAll()使用

14、為什麼wait,nofity和nofityAll這些方法不放在Thread類當中

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

15、怎麼喚醒一個阻塞的執行緒

如果執行緒是因為呼叫了wait()、sleep()或者join()方法而導致的阻塞,可以中斷執行緒,並且通過丟擲InterruptedException來喚醒它;如果執行緒遇到了IO阻塞,無能為力,因為IO是作業系統實現的,Java程式碼並沒有辦法直接接觸到作業系統。

16、什麼是多執行緒的上下文切換

多執行緒的上下文切換是指CPU控制權由一個已經正在執行的執行緒切換到另外一個就緒並等待獲取CPU執行權的執行緒的過程。

17、synchronized和ReentrantLock的區別

synchronized是和if、else、for、while一樣的關鍵字,ReentrantLock是類,這是二者的本質區別。既然ReentrantLock是類,那麼它就提供了比synchronized更多更靈活的特性,可以被繼承、可以有方法、可以有各種各樣的類變數,ReentrantLock比synchronized的擴充套件性體現在幾點上:
(1)ReentrantLock可以對獲取鎖的等待時間進行設定,這樣就避免了死鎖
(2)ReentrantLock可以獲取各種鎖的資訊
(3)ReentrantLock可以靈活地實現多路通知
另外,二者的鎖機制其實也是不一樣的:ReentrantLock底層呼叫的是Unsafe的park方法加鎖,synchronized操作的應該是物件頭中mark word.

18、FutureTask是什麼

這個其實前面有提到過,FutureTask表示一個非同步運算的任務。FutureTask裡面可以傳入一個Callable的具體實現類,可以對這個非同步運算的任務的結果進行等待獲取、判斷是否已經完成、取消任務等操作。當然,由於FutureTask也是Runnable介面的實現類,所以FutureTask也可以放入執行緒池中。

19、一個執行緒如果出現了執行時異常怎麼辦?

如果這個異常沒有被捕獲的話,這個執行緒就停止執行了。另外重要的一點是:如果這個執行緒持有某個某個物件的監視器,那麼這個物件監視器會被立即釋放

20、Java當中有哪幾種鎖

  • 自旋鎖: 自旋鎖在JDK1.6之後就預設開啟了。基於之前的觀察,共享資料的鎖定狀態只會持續很短的時間,為了這一小段時間而去掛起和恢復執行緒有點浪費,所以這裡就做了一個處理,讓後面請求鎖的那個執行緒在稍等一會,但是不放棄處理器的執行時間,看看持有鎖的執行緒能否快速釋放。為了讓執行緒等待,所以需要讓執行緒執行一個忙迴圈也就是自旋操作。在jdk6之後,引入了自適應的自旋鎖,也就是等待的時間不再固定了,而是由上一次在同一個鎖上的自旋時間及鎖的擁有者狀態來決定

  • 偏向鎖: 在JDK1.之後引入的一項鎖優化,目的是消除資料在無競爭情況下的同步原語。進一步提升程式的執行效能。 偏向鎖就是偏心的偏,意思是這個鎖會偏向第一個獲得他的執行緒,如果接下來的執行過程中,改鎖沒有被其他執行緒獲取,則持有偏向鎖的執行緒將永遠不需要再進行同步。偏向鎖可以提高帶有同步但無競爭的程式效能,也就是說他並不一定總是對程式執行有利,如果程式中大多數的鎖都是被多個不同的執行緒訪問,那偏向模式就是多餘的,在具體問題具體分析的前提下,可以考慮是否使用偏向鎖。

  • 輕量級鎖: 為了減少獲得鎖和釋放鎖所帶來的效能消耗,引入了“偏向鎖”和“輕量級鎖”,所以在Java SE1.6裡鎖一共有四種狀態,無鎖狀態,偏向鎖狀態,輕量級鎖狀態和重量級鎖狀態,它會隨著競爭情況逐漸升級。鎖可以升級但不能降級,意味著偏向鎖升級成輕量級鎖後不能降級成偏向鎖

21、如何在兩個執行緒間共享資料

通過線上程之間共享物件就可以了,然後通過wait/notify/notifyAll、await/signal/signalAll進行喚起和等待,比方說阻塞佇列BlockingQueue就是為執行緒之間共享資料而設計的

22、如何正確的使用wait()?使用if還是while?

wait() 方法應該在迴圈呼叫,因為當執行緒獲取到 CPU 開始執行的時候,其他條件可能還沒有滿足,所以在處理前,迴圈檢測條件是否滿足會更好。下面是一段標準的使用 wait 和 notify 方法的程式碼:

synchronized (obj) {
   while (condition does not hold)
     obj.wait(); // (Releases lock, and reacquires on wakeup)
     ... // Perform action appropriate to condition
}
複製程式碼

23、什麼是執行緒區域性變數ThreadLocal

執行緒區域性變數是侷限於執行緒內部的變數,屬於執行緒自身所有,不在多個執行緒間共享。Java提供ThreadLocal類來支援執行緒區域性變數,是一種實現執行緒安全的方式。但是在管理環境下(如 web 伺服器)使用執行緒區域性變數的時候要特別小心,在這種情況下,工作執行緒的生命週期比任何應用變數的生命週期都要長。任何執行緒區域性變數一旦在工作完成後沒有釋放,Java 應用就存在記憶體洩露的風險。

24、ThreadLoal的作用是什麼?

簡單說ThreadLocal就是一種以空間換時間的做法在每個Thread裡面維護了一個ThreadLocal.ThreadLocalMap把資料進行隔離,資料不共享,自然就沒有執行緒安全方面的問題了.

25、生產者消費者模型的作用是什麼?

(1)通過平衡生產者的生產能力和消費者的消費能力來提升整個系統的執行效率,這是生產者消費者模型最重要的作用
(2)解耦,這是生產者消費者模型附帶的作用,解耦意味著生產者和消費者之間的聯絡少,聯絡越少越可以獨自發展而不需要收到相互的制約

26.寫一個生產者-消費者佇列

可以通過阻塞佇列實現,也可以通過wait-notify來實現.
使用阻塞佇列來實現

//消費者
public class Producer implements Runnable{
   private final BlockingQueue<Integer> queue;

   public Producer(BlockingQueue q){
       this.queue=q;
   }

   @Override
   public void run() {
       try {
           while (true){
               Thread.sleep(1000);//模擬耗時
               queue.put(produce());
           }
       }catch (InterruptedException e){

       }
   }

   private int produce() {
       int n=new Random().nextInt(10000);
       System.out.println("Thread:" + Thread.currentThread().getId() + " produce:" + n);
       return n;
   }
}
//消費者
public class Consumer implements Runnable {
   private final BlockingQueue<Integer> queue;

   public Consumer(BlockingQueue q){
       this.queue=q;
   }

   @Override
   public void run() {
       while (true){
           try {
               Thread.sleep(2000);//模擬耗時
               consume(queue.take());
           }catch (InterruptedException e){

           }

       }
   }

   private void consume(Integer n) {
       System.out.println("Thread:" + Thread.currentThread().getId() + " consume:" + n);

   }
}
//測試
public class Main {

   public static void main(String[] args) {
       BlockingQueue<Integer> queue=new ArrayBlockingQueue<Integer>(100);
       Producer p=new Producer(queue);
       Consumer c1=new Consumer(queue);
       Consumer c2=new Consumer(queue);

       new Thread(p).start();
       new Thread(c1).start();
       new Thread(c2).start();
   }
}
複製程式碼

使用wait-notify來實現

該種方式應該最經典,這裡就不做說明了

27、如果你提交任務時,執行緒池佇列已滿,這時會發生什麼

如果你使用的LinkedBlockingQueue,也就是無界佇列的話,沒關係,繼續新增任務到阻塞佇列中等待執行,因為LinkedBlockingQueue可以近乎認為是一個無窮大的佇列,可以無限存放任務;如果你使用的是有界佇列比方說ArrayBlockingQueue的話,任務首先會被新增到ArrayBlockingQueue中,ArrayBlockingQueue滿了,則會使用拒絕策略RejectedExecutionHandler處理滿了的任務,預設是AbortPolicy。

28、為什麼要使用執行緒池

避免頻繁地建立和銷燬執行緒,達到執行緒物件的重用。另外,使用執行緒池還可以根據專案靈活地控制併發的數目。

29、java中用到的執行緒排程演算法是什麼

搶佔式。一個執行緒用完CPU之後,作業系統會根據執行緒優先順序、執行緒飢餓情況等資料算出一個總的優先順序並分配下一個時間片給某個執行緒執行。

30、Thread.sleep(0)的作用是什麼

由於Java採用搶佔式的執行緒排程演算法,因此可能會出現某條執行緒常常獲取到CPU控制權的情況,為了讓某些優先順序比較低的執行緒也能獲取到CPU控制權,可以使用Thread.sleep(0)手動觸發一次作業系統分配時間片的操作,這也是平衡CPU控制權的一種操作。

31、什麼是CAS

CAS,全稱為Compare and Swap,即比較-替換。假設有三個運算元:記憶體值V、舊的預期值A、要修改的值B,當且僅當預期值A和記憶體值V相同時,才會將記憶體值修改為B並返回true,否則什麼都不做並返回false。當然CAS一定要volatile變數配合,這樣才能保證每次拿到的變數是主記憶體中最新的那個值,否則舊的預期值A對某條執行緒來說,永遠是一個不會變的值A,只要某次CAS操作失敗,永遠都不可能成功

32、什麼是樂觀鎖和悲觀鎖

樂觀鎖:樂觀鎖認為競爭不總是會發生,因此它不需要持有鎖,將比較-替換這兩個動作作為一個原子操作嘗試去修改記憶體中的變數,如果失敗則表示發生衝突,那麼就應該有相應的重試邏輯。

悲觀鎖:悲觀鎖認為競爭總是會發生,因此每次對某資源進行操作時,都會持有一個獨佔的鎖,就像synchronized,不管三七二十一,直接上了鎖就操作資源了。

33、ConcurrentHashMap的併發度是什麼?

ConcurrentHashMap的併發度就是segment的大小,預設為16,這意味著最多同時可以有16條執行緒操作ConcurrentHashMap,這也是ConcurrentHashMap對Hashtable的最大優勢,任何情況下,Hashtable能同時有兩條執行緒獲取Hashtable中的資料嗎?

34、ConcurrentHashMap的工作原理

ConcurrentHashMap在jdk 1.6和jdk 1.8實現原理是不同的.
jdk 1.6:

ConcurrentHashMap是執行緒安全的,但是與Hashtablea相比,實現執行緒安全的方式不同。Hashtable是通過對hash表結構進行鎖定,是阻塞式的,當一個執行緒佔有這個鎖時,其他執行緒必須阻塞等待其釋放鎖。ConcurrentHashMap是採用分離鎖的方式,它並沒有對整個hash表進行鎖定,而是區域性鎖定,也就是說當一個執行緒佔有這個區域性鎖時,不影響其他執行緒對hash表其他地方的訪問。
具體實現:ConcurrentHashMap內部有一個Segment
jdk 1.8

在jdk 8中,ConcurrentHashMap不再使用Segment分離鎖,而是採用一種樂觀鎖CAS演算法來實現同步問題,但其底層還是“陣列+連結串列->紅黑樹”的實現。

37、CyclicBarrier和CountDownLatch區別

這兩個類非常類似,都在java.util.concurrent下,都可以用來表示程式碼執行到某個點上,二者的區別在於:

  • CyclicBarrier的某個執行緒執行到某個點上之後,該執行緒即停止執行,直到所有的執行緒都到達了這個點,所有執行緒才重新執行;CountDownLatch則不是,某執行緒執行到某個點上之後,只是給某個數值-1而已,該執行緒繼續執行

  • CyclicBarrier只能喚起一個任務,CountDownLatch可以喚起多個任務

  • CyclicBarrier可重用,CountDownLatch不可重用,計數值為0該CountDownLatch就不可再用了

39、java中的++操作符執行緒安全麼?

不是執行緒安全的操作。它涉及到多個指令,如讀取變數值,增加,然後儲存回記憶體,這個過程可能會出現多個執行緒交差

40、你有哪些多執行緒開發良好的實踐?

  • 給執行緒命名

  • 最小化同步範圍

  • 優先使用volatile

  • 儘可能使用更高層次的併發工具而非wait和notify()來實現執行緒通訊,如BlockingQueue,Semeaphore

  • 優先使用併發容器而非同步容器.

  • 考慮使用執行緒池

關於volatile關鍵字

1、可以建立Volatile陣列嗎?

Java 中可以建立 volatile型別陣列,不過只是一個指向陣列的引用,而不是整個陣列。如果改變引用指向的陣列,將會受到volatile 的保護,但是如果多個執行緒同時改變陣列的元素,volatile標示符就不能起到之前的保護作用了

2、volatile能使得一個非原子操作變成原子操作嗎?

一個典型的例子是在類中有一個 long 型別的成員變數。如果你知道該成員變數會被多個執行緒訪問,如計數器、價格等,你最好是將其設定為 volatile。為什麼?因為 Java 中讀取 long 型別變數不是原子的,需要分成兩步,如果一個執行緒正在修改該 long 變數的值,另一個執行緒可能只能看到該值的一半(前 32 位)。但是對一個 volatile 型的 long 或 double 變數的讀寫是原子。

一種實踐是用 volatile 修飾 long 和 double 變數,使其能按原子型別來讀寫。double 和 long 都是64位寬,因此對這兩種型別的讀是分為兩部分的,第一次讀取第一個 32 位,然後再讀剩下的 32 位,這個過程不是原子的,但 Java 中 volatile 型的 long 或 double 變數的讀寫是原子的。volatile 修復符的另一個作用是提供記憶體屏障(memory barrier),例如在分散式框架中的應用。簡單的說,就是當你寫一個 volatile 變數之前,Java 記憶體模型會插入一個寫屏障(write barrier),讀一個 volatile 變數之前,會插入一個讀屏障(read barrier)。意思就是說,在你寫一個 volatile 域時,能保證任何執行緒都能看到你寫的值,同時,在寫之前,也能保證任何數值的更新對所有執行緒是可見的,因為記憶體屏障會將其他所有寫的值更新到快取。

3、volatile型別變數提供什麼保證?

volatile 主要有兩方面的作用:1.避免指令重排2.可見性保證.例如,JVM 或者 JIT為了獲得更好的效能會對語句重排序,但是 volatile 型別變數即使在沒有同步塊的情況下賦值也不會與其他語句重排序。 volatile 提供 happens-before 的保證,確保一個執行緒的修改能對其他執行緒是可見的。某些情況下,volatile 還能提供原子性,如讀 64 位資料型別,像 long 和 double 都不是原子的(低32位和高32位),但 volatile 型別的 double 和 long 就是原子的.

bat等大公司常考java多執行緒面試題

微信掃一掃關注公眾號【好好學java】,優質文章第一時間瞭解

相關文章