bat等大公司常考java多線程面試題

一、說說進程,線程,協程之間的區別

簡而言之,進程是程序運行和資源分配的基本單位,一個程序至少有一個進程,一個進程至少有一個線程.進程在執行過程當中擁有獨立的內存單元,而多個線程共享內存資源,減小切換次數,從而效率更高.線程是進程的一個實體,是cpu調度和分派的基本單位,是比程序更小的能獨立運行的基本單位.同一進程中的多個線程之間能夠併發執行.java

二、你瞭解守護線程嗎?它和非守護線程有什麼區別

程序運行完畢,jvm會等待非守護線程完成後關閉,可是jvm不會等待守護線程.守護線程最典型的例子就是GC線程web

三、什麼是多線程上下文切換

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

四、建立兩種線程的方式?他們有什麼區別?編程

經過實現java.lang.Runnable或者經過擴展java.lang.Thread類.相比擴展Thread,實現Runnable接口可能更優.緣由有二:數組

  • Java不支持多繼承.所以擴展Thread類就表明這個子類不能擴展其餘類.而實現Runnable接口的類還可能擴展另外一個類.
  • 類可能只要求可執行便可,所以繼承整個Thread類的開銷過大.
五、Thread類中的start()和run()方法有什麼區別?

start()方法被用來啓動新建立的線程,並且start()內部調用了run()方法,這和直接調用run()方法的效果不同。當你調用run()方法的時候,只會是在原來的線程中調用,沒有新的線程啓動,start()方法纔會啓動新線程。緩存

六、怎麼檢測一個線程是否持有對象監視器

Thread類提供了一個holdsLock(Object obj)方法,當且僅當對象obj的監視器被某條線程持有的時候纔會返回true,注意這是一個static方法,這意味着」某條線程」指的是當前線程。安全

七、Runnable和Callable的區別

Runnable接口中的run()方法的返回值是void,它作的事情只是純粹地去執行run()方法中的代碼而已;Callable接口中的call()方法是有返回值的,是一個泛型,和Future、FutureTask配合能夠用來獲取異步執行的結果。
這實際上是頗有用的一個特性,由於多線程相比單線程更難、更復雜的一個重要緣由就是由於多線程充滿着未知性,某條線程是否執行了?某條線程執行了多久?某條線程執行的時候咱們指望的數據是否已經賦值完畢?沒法得知,咱們能作的只是等待這條多線程的任務執行完畢而已。而Callable+Future/FutureTask卻能夠方便獲取多線程運行的結果,能夠在等待時間太長沒獲取到須要的數據的狀況下取消該線程的任務服務器

八、什麼致使線程阻塞

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

方法 說明
sleep() sleep() 容許 指定以毫秒爲單位的一段時間做爲參數,它使得線程在指定的時間內進入阻塞狀態,不能獲得CPU 時間,指定的時間一過,線程從新進入可執行狀態。 典型地,sleep() 被用在等待某個資源就緒的情形:測試發現條件不知足後,讓線程阻塞一段時間後從新測試,直到條件知足爲止
suspend() 和 resume() 兩個方法配套使用,suspend()使得線程進入阻塞狀態,而且不會自動恢復,必須其對應的resume() 被調用,才能使得線程從新進入可執行狀態。典型地,suspend() 和 resume() 被用在等待另外一個線程產生的結果的情形:測試發現結果尚未產生後,讓線程阻塞,另外一個線程產生告終果後,調用 resume() 使其恢復。
yield() yield() 使當前線程放棄當前已經分得的CPU 時間,但不使當前線程阻塞,即線程仍處於可執行狀態,隨時可能再次分得 CPU 時間。調用 yield() 的效果等價於調度程序認爲該線程已執行了足夠的時間從而轉到另外一個線程
wait() 和 notify() 兩個方法配套使用,wait() 使得線程進入阻塞狀態,它有兩種形式,一種容許 指定以毫秒爲單位的一段時間做爲參數,另外一種沒有參數,前者當對應的 notify() 被調用或者超出指定時間時線程從新進入可執行狀態,後者則必須對應的 notify() 被調用.
九、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() 方法,由於它們的功能最強大,使用也最靈活,可是這也致使了它們的效率較低,較容易出錯。實際使用中咱們應該靈活使用各類方法,以便更好地達到咱們的目的。

十一、產生死鎖的條件

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

十二、爲何wait()方法和notify()/notifyAll()方法要在同步塊中被調用

這是JDK強制的,wait()方法和notify()/notifyAll()方法在調用前都必須先得到對象的鎖
wait()方法和notify()/notifyAll()方法在放棄對象監視器時有什麼區別

wait()方法和notify()/notifyAll()方法在放棄對象監視器的時候的區別在於:wait()方法當即釋放對象監視器,notify()/notifyAll()方法則會等待線程剩餘代碼執行完畢纔會放棄對象監視器。

1三、wait()與sleep()的區別

關於這二者已經在上面進行詳細的說明,這裏就作個歸納好了:

  • sleep()來自Thread類,和wait()來自Object類.調用sleep()方法的過程當中,線程不會釋放對象鎖。而 調用 wait 方法線程會釋放對象鎖
  • sleep()睡眠後不出讓系統資源,wait讓其餘線程能夠佔用CPU
  • sleep(milliseconds)須要指定一個睡眠時間,時間一到會自動喚醒.而wait()須要配合notify()或者notifyAll()使用
1四、爲何wait,nofity和nofityAll這些方法不放在Thread類當中

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

1五、怎麼喚醒一個阻塞的線程

若是線程是由於調用了wait()、sleep()或者join()方法而致使的阻塞,能夠中斷線程,而且經過拋出InterruptedException來喚醒它;若是線程遇到了IO阻塞,無能爲力,由於IO是操做系統實現的,Java代碼並無辦法直接接觸到操做系統。

1六、什麼是多線程的上下文切換

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

1七、synchronized和ReentrantLock的區別

synchronized是和if、else、for、while同樣的關鍵字,ReentrantLock是類,這是兩者的本質區別。既然ReentrantLock是類,那麼它就提供了比synchronized更多更靈活的特性,能夠被繼承、能夠有方法、能夠有各類各樣的類變量,ReentrantLock比synchronized的擴展性體如今幾點上:
(1)ReentrantLock能夠對獲取鎖的等待時間進行設置,這樣就避免了死鎖
(2)ReentrantLock能夠獲取各類鎖的信息
(3)ReentrantLock能夠靈活地實現多路通知
另外,兩者的鎖機制其實也是不同的:ReentrantLock底層調用的是Unsafe的park方法加鎖,synchronized操做的應該是對象頭中mark word.

1八、FutureTask是什麼

這個其實前面有提到過,FutureTask表示一個異步運算的任務。FutureTask裏面能夠傳入一個Callable的具體實現類,能夠對這個異步運算的任務的結果進行等待獲取、判斷是否已經完成、取消任務等操做。固然,因爲FutureTask也是Runnable接口的實現類,因此FutureTask也能夠放入線程池中。

1九、一個線程若是出現了運行時異常怎麼辦?

若是這個異常沒有被捕獲的話,這個線程就中止執行了。另外重要的一點是:若是這個線程持有某個某個對象的監視器,那麼這個對象監視器會被當即釋放

20、Java當中有哪幾種鎖
  • 自旋鎖: 自旋鎖在JDK1.6以後就默認開啓了。基於以前的觀察,共享數據的鎖定狀態只會持續很短的時間,爲了這一小段時間而去掛起和恢復線程有點浪費,因此這裏就作了一個處理,讓後面請求鎖的那個線程在稍等一會,可是不放棄處理器的執行時間,看看持有鎖的線程可否快速釋放。爲了讓線程等待,因此須要讓線程執行一個忙循環也就是自旋操做。在jdk6以後,引入了自適應的自旋鎖,也就是等待的時間再也不固定了,而是由上一次在同一個鎖上的自旋時間及鎖的擁有者狀態來決定
  • 偏向鎖: 在JDK1.以後引入的一項鎖優化,目的是消除數據在無競爭狀況下的同步原語。進一步提高程序的運行性能。 偏向鎖就是偏愛的偏,意思是這個鎖會偏向第一個得到他的線程,若是接下來的執行過程當中,改鎖沒有被其餘線程獲取,則持有偏向鎖的線程將永遠不須要再進行同步。偏向鎖能夠提升帶有同步但無競爭的程序性能,也就是說他並不必定老是對程序運行有利,若是程序中大多數的鎖都是被多個不一樣的線程訪問,那偏向模式就是多餘的,在具體問題具體分析的前提下,能夠考慮是否使用偏向鎖。
  • 輕量級鎖: 爲了減小得到鎖和釋放鎖所帶來的性能消耗,引入了「偏向鎖」和「輕量級鎖」,因此在Java SE1.6裏鎖一共有四種狀態,無鎖狀態,偏向鎖狀態,輕量級鎖狀態和重量級鎖狀態,它會隨着競爭狀況逐漸升級。鎖能夠升級但不能降級,意味着偏向鎖升級成輕量級鎖後不能降級成偏向鎖
2一、如何在兩個線程間共享數據

經過在線程之間共享對象就能夠了,而後經過wait/notify/notifyAll、await/signal/signalAll進行喚起和等待,比方說阻塞隊列BlockingQueue就是爲線程之間共享數據而設計的

2二、如何正確的使用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
}
2三、什麼是線程局部變量ThreadLocal

線程局部變量是侷限於線程內部的變量,屬於線程自身全部,不在多個線程間共享。Java提供ThreadLocal類來支持線程局部變量,是一種實現線程安全的方式。可是在管理環境下(如 web 服務器)使用線程局部變量的時候要特別當心,在這種狀況下,工做線程的生命週期比任何應用變量的生命週期都要長。任何線程局部變量一旦在工做完成後沒有釋放,Java 應用就存在內存泄露的風險。

2四、ThreadLoal的做用是什麼?

簡單說ThreadLocal就是一種以空間換時間的作法在每一個Thread裏面維護了一個ThreadLocal.ThreadLocalMap把數據進行隔離,數據不共享,天然就沒有線程安全方面的問題了.

2五、生產者消費者模型的做用是什麼?

(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來實現

該種方式應該最經典,這裏就不作說明了

2七、若是你提交任務時,線程池隊列已滿,這時會發生什麼

若是你使用的LinkedBlockingQueue,也就是無界隊列的話,不要緊,繼續添加任務到阻塞隊列中等待執行,由於LinkedBlockingQueue能夠近乎認爲是一個無窮大的隊列,能夠無限存聽任務;若是你使用的是有界隊列比方說ArrayBlockingQueue的話,任務首先會被添加到ArrayBlockingQueue中,ArrayBlockingQueue滿了,則會使用拒絕策略RejectedExecutionHandler處理滿了的任務,默認是AbortPolicy。

2八、爲何要使用線程池

避免頻繁地建立和銷燬線程,達到線程對象的重用。另外,使用線程池還能夠根據項目靈活地控制併發的數目。

2九、java中用到的線程調度算法是什麼

搶佔式。一個線程用完CPU以後,操做系統會根據線程優先級、線程飢餓狀況等數據算出一個總的優先級並分配下一個時間片給某個線程執行。

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

因爲Java採用搶佔式的線程調度算法,所以可能會出現某條線程經常獲取到CPU控制權的狀況,爲了讓某些優先級比較低的線程也能獲取到CPU控制權,可使用Thread.sleep(0)手動觸發一次操做系統分配時間片的操做,這也是平衡CPU控制權的一種操做。

3一、什麼是CAS

CAS,全稱爲Compare and Swap,即比較-替換。假設有三個操做數:內存值V、舊的預期值A、要修改的值B,當且僅當預期值A和內存值V相同時,纔會將內存值修改成B並返回true,不然什麼都不作並返回false。固然CAS必定要volatile變量配合,這樣才能保證每次拿到的變量是主內存中最新的那個值,不然舊的預期值A對某條線程來講,永遠是一個不會變的值A,只要某次CAS操做失敗,永遠都不可能成功

3二、什麼是樂觀鎖和悲觀鎖

樂觀鎖:樂觀鎖認爲競爭不老是會發生,所以它不須要持有鎖,將比較-替換這兩個動做做爲一個原子操做嘗試去修改內存中的變量,若是失敗則表示發生衝突,那麼就應該有相應的重試邏輯。

悲觀鎖:悲觀鎖認爲競爭老是會發生,所以每次對某資源進行操做時,都會持有一個獨佔的鎖,就像synchronized,無論三七二十一,直接上了鎖就操做資源了。

3三、ConcurrentHashMap的併發度是什麼?

ConcurrentHashMap的併發度就是segment的大小,默認爲16,這意味着最多同時能夠有16條線程操做ConcurrentHashMap,這也是ConcurrentHashMap對Hashtable的最大優點,任何狀況下,Hashtable能同時有兩條線程獲取Hashtable中的數據嗎?

3四、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算法來實現同步問題,但其底層仍是「數組+鏈表->紅黑樹」的實現。

3七、CyclicBarrier和CountDownLatch區別

這兩個類很是相似,都在java.util.concurrent下,均可以用來表示代碼運行到某個點上,兩者的區別在於:

  • CyclicBarrier的某個線程運行到某個點上以後,該線程即中止運行,直到全部的線程都到達了這個點,全部線程才從新運行;CountDownLatch則不是,某線程運行到某個點上以後,只是給某個數值-1而已,該線程繼續運行
  • CyclicBarrier只能喚起一個任務,CountDownLatch能夠喚起多個任務
  • CyclicBarrier可重用,CountDownLatch不可重用,計數值爲0該CountDownLatch就不可再用了
3九、java中的++操做符線程安全麼?

不是線程安全的操做。它涉及到多個指令,如讀取變量值,增長,而後存儲回內存,這個過程可能會出現多個線程交差

40、你有哪些多線程開發良好的實踐?
  • 給線程命名
  • 最小化同步範圍
  • 優先使用volatile
  • 儘量使用更高層次的併發工具而非wait和notify()來實現線程通訊,如BlockingQueue,Semeaphore
  • 優先使用併發容器而非同步容器.
  • 考慮使用線程池

關於volatile關鍵字

一、能夠建立Volatile數組嗎?

Java 中能夠建立 volatile類型數組,不過只是一個指向數組的引用,而不是整個數組。若是改變引用指向的數組,將會受到volatile 的保護,可是若是多個線程同時改變數組的元素,volatile標示符就不能起到以前的保護做用了

二、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 域時,能保證任何線程都能看到你寫的值,同時,在寫以前,也能保證任何數值的更新對全部線程是可見的,由於內存屏障會將其餘全部寫的值更新到緩存。

三、volatile類型變量提供什麼保證?

volatile 主要有兩方面的做用:1.避免指令重排2.可見性保證.例如,JVM 或者 JIT爲了得到更好的性能會對語句重排序,可是 volatile 類型變量即便在沒有同步塊的狀況下賦值也不會與其餘語句重排序。 volatile 提供 happens-before 的保證,確保一個線程的修改能對其餘線程是可見的。某些狀況下,volatile 還能提供原子性,如讀 64 位數據類型,像 long 和 double 都不是原子的(低32位和高32位),但 volatile 類型的 double 和 long 就是原子的.

這裏寫圖片描述

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

相關文章
相關標籤/搜索