天天看點

多線程常見面試題總結

1.線程和程序

線程 

 這裡所說的線程指程式執行過程中的一個線程實體。JVM 允許一個應用并發執行多個線程。Hotspot JVM 中的 Java 線程與原生作業系統線程有直接的映射關系。當線程本地存儲、緩沖區配置設定、同步對象、棧、程式計數器等準備好以後,就會建立一個作業系統原生線程。Java 線程結束,原生線程随之被回收。作業系統負責排程所有線程,并把它們配置設定到任何可用的 CPU 上。當原生線程初始化完畢,就會調用 Java 線程的 run() 方法。當線程結束時,會釋放原生線程和 Java 線程的所有資源。

程序

  程序是可并發執行的程式在某個資料集合上的一次計算活動,也是作業系統進行資源配置設定和排程的基本機關。

程序與程式的聯系與差別

① 程式是指令的有序集合,其本身沒有任何運作的含義,是一個靜态的概念。而程序是程式在處理機上的一次執行過程,它是一個動态的概念。

② 程式可以作為一種軟體資料長期存在,而程序是有一定生命期的。程式是永久的,程序是暫時的。

注:程式可看作一個菜單,而程序則是按照菜單進行烹調的過程。

③ 程序和程式組成不同:程序是由程式、資料和程序控制塊三部分組成的。

④ 程序與程式的對應關系:通過多次執行,一個程式可對應多個程序;通過調用關系,一個程序可包括多個程式。

2.并發和并行之間的差別

  • 解釋一:并行是指兩個或者多個事件在同一時刻發生;而并發是指兩個或多個事件在同一時間間隔發生。
  • 解釋二:并行是在不同實體上的多個事件,并發是在同一實體上的多個事件。
  • 解釋三:并行是在一台處理器上“同時”處理多個任務,并發是在多台處理器上同時處理多個任務。如 hadoop 分布式叢集。

3.線程的建立方式

繼承 Thread 類

  Thread 類本質上是實作了 Runnable 接口的一個執行個體,代表一個線程的執行個體。啟動線程的唯一方法就是通過 Thread 類的 start()執行個體方法。start()方法是一個 native 方法,它将啟動一個新線程,并執行 run()方法。

實作 Runnable 接口。

  如果自己的類已經 extends 另一個類,就無法直接 extends Thread,此時,可以實作一個Runnable 接口

ExecutorService、Callable<Class>、Future 有傳回值線程

  有傳回值的任務必須實作 Callable 接口,類似的,無傳回值的任務必須 Runnable 接口。執行Callable 任務後,可以擷取一個 Future 的對象,在該對象上調用 get 就可以擷取到 Callable 任務傳回的 Object 了,再結合線程池接口 ExecutorService 就可以實作傳說中有傳回結果的多線程了。 

基于線程池的方式

  線程和資料庫連接配接這些資源都是非常寶貴的資源。那麼每次需要的時候建立,不需要的時候銷毀,是非常浪費資源的。那麼我們就可以使用緩存的政策,也就是使用線程池。

4.線程池建立的方式

newCachedThreadPool

  建立一個可根據需要建立新線程的線程池,但是在以前構造的線程可用時将重用它們。對于執行很多短期異步任務的程式而言,這些線程池通常可提高程式性能。調用 execute 将重用以前構造的線程(如果線程可用)。如果現有線程沒有可用的,則建立一個新線程并添加到池中。終止并從緩存中移除那些已有 60 秒鐘未被使用的線程。是以,長時間保持空閑的線程池不會使用任何資源。

newFixedThreadPool

  建立一個可重用固定線程數的線程池,以共享的無界隊列方式來運作這些線程。在任意點,在大多數 nThreads 線程會處于處理任務的活動狀态。如果在所有線程處于活動狀态時送出附加任務,則在有可用線程之前,附加任務将在隊列中等待。如果在關閉前的執行期間由于失敗而導緻任何線程終止,那麼一個新線程将代替它執行後續的任務(如果需要)。在某個線程被顯式地關閉之前,池中的線程将一直存在。

newScheduledThreadPool

  建立一個線程池,它可安排在給定延遲後運作指令或者定期地執行。 

ScheduledExecutorService scheduledThreadPool= Executors.newScheduledThreadPool(3); 
 scheduledThreadPool.schedule(newRunnable(){ 
 @Override 
 public void run() {
 System.out.println("延遲三秒");
 }
 }, 3, TimeUnit.SECONDS);
scheduledThreadPool.scheduleAtFixedRate(newRunnable(){ 
 @Override 
 public void run() {
 System.out.println("延遲 1 秒後每三秒執行一次");
 }
 },1,3,TimeUnit.SECONDS);      

newSingleThreadExecutor

  Executors.newSingleThreadExecutor()傳回一個線程池(這個線程池隻有一個線程),這個線程池可以線上程死後(或發生異常時)重新啟動一個線程來替代原來的線程繼續執行下去! 

5.線程生命周期(狀态) 

  當線程被建立并啟動以後,它既不是一啟動就進入了執行狀态,也不是一直處于執行狀态。線上程的生命周期中,它要經過建立(New)、就緒(Runnable)、運作(Running)、阻塞(Blocked)和死亡(Dead)5 種狀态。尤其是當線程啟動以後,它不可能一直"霸占"着 CPU 獨自運作,是以 CPU 需要在多條線程之間切換,于是線程狀态也會多次在運作、阻塞之間切換

建立狀态(NEW)

  當程式使用 new 關鍵字建立了一個線程之後,該線程就處于建立狀态,此時僅由 JVM 為其配置設定記憶體,并初始化其成員變量的值

就緒狀态(RUNNABLE)

  當線程對象調用了 start()方法之後,該線程處于就緒狀态。Java 虛拟機會為其建立方法調用棧和程式計數器,等待排程運作

運作狀态(RUNNING):

  如果處于就緒狀态的線程獲得了 CPU,開始執行 run()方法的線程執行體,則該線程處于運作狀态

阻塞狀态(BLOCKED):

  阻塞狀态是指線程因為某種原因放棄了 cpu 使用權,也即讓出了 cpu timeslice,暫時停止運作。直到線程進入可運作(runnable)狀态,才有機會再次獲得 cpu timeslice 轉到運作(running)狀

态。阻塞的情況分三種:

  等待阻塞(o.wait->等待對列):

    運作(running)的線程執行 o.wait()方法,JVM 會把該線程放入等待隊列(waitting queue)中。

  同步阻塞(lock->鎖池)

    運作(running)的線程在擷取對象的同步鎖時,若該同步鎖被别的線程占用,則 JVM 會把該線程放入鎖池(lock pool)中。

  其他阻塞(sleep/join)

    運作(running)的線程執行 Thread.sleep(long ms)或 t.join()方法,或者發出了 I/O 請求時,JVM 會把該線程置為阻塞狀态。當 sleep()狀态逾時、join()等待線程終止或者逾時、或者 I/O

    處理完畢時,線程重新轉入可運作(runnable)狀态。 

線程死亡(DEAD)

線程會以下面三種方式結束,結束後就是死亡狀态。

正常結束

1. run()或 call()方法執行完成,線程正常結束。

異常結束

2. 線程抛出一個未捕獲的 Exception 或 Error。

調用 stop

3. 直接調用該線程的 stop()方法來結束該線程—該方法通常容易導緻死鎖,不推薦使用。

多線程常見面試題總結

 6.sleep 與 wait 差別

1. 對于 sleep()方法,我們首先要知道該方法是屬于 Thread 類中的。而 wait()方法,則是屬于Object 類中的。

2. sleep()方法導緻了程式暫停執行指定的時間,讓出 cpu 該其他線程,但是他的監控狀态依然保持者,當指定的時間到了又會自動恢複運作狀态。

3. 在調用 sleep()方法的過程中,線程不會釋放對象鎖。

4. 而當調用 wait()方法的時候,線程會放棄對象鎖,進入等待此對象的等待鎖定池,隻有針對此對象調用 notify()方法後本線程才進入對象鎖定池準備擷取對象鎖進入運作狀态

7.start 與 run 差別

1. start()方法來啟動線程,真正實作了多線程運作。這時無需等待 run 方法體代碼執行完畢,可以直接繼續執行下面的代碼。

2. 通過調用 Thread 類的 start()方法來啟動一個線程, 這時此線程是處于就緒狀态, 并沒有運作。

3. 方法 run()稱為線程體,它包含了要執行的這個線程的内容,線程就進入了運作狀态,開始運作 run 函數當中的代碼。 Run 方法運作結束, 此線程終止。然後 CPU 再排程其它線程。

8.volatile 關鍵字的作用(變量可見性、禁止重排序)

  Java 語言提供了一種稍弱的同步機制,即 volatile 變量,用來確定将變量的更新操作通知到其他線程。volatile 變量具備兩種特性,volatile 變量不會被緩存在寄存器或者對其他處理器不可見的地方,是以在讀取 volatile 類型的變量時總會傳回最新寫入的值。

變量可見性

  其一是保證該變量對所有線程可見,這裡的可見性指的是當一個線程修改了變量的值,那麼新的值對于其他線程是可以立即擷取的。

禁止重排序

  volatile 禁止了指令重排。比 sychronized 更輕量級的同步鎖在通路 volatile 變量時不會執行加鎖操作,是以也就不會使執行線程阻塞,是以 volatile 變量是一種比 sychronized 關鍵字更輕量級的同步機制。volatile 适合這種場景:一個變量被多個線程共享,線程直接給這個變量指派。 

  當對非 volatile 變量進行讀寫的時候,每個線程先從記憶體拷貝變量到 CPU 緩存中。如果計算機有多個 CPU,每個線程可能在不同的 CPU 上被處理,這意味着每個線程可以拷貝到不同的 CPUcache 中。而聲明變量是 volatile 的,JVM 保證了每次讀變量都從記憶體中讀,跳過 CPU cache這一步。

适用場景

  值得說明的是對 volatile 變量的單次讀/寫操作可以保證原子性的,如 long 和 double 類型變量,但是并不能保證 i++這種操作的原子性,因為本質上 i++是讀、寫兩次操作。在某些場景下可以代替 Synchronized。但是,volatile 的不能完全取代 Synchronized 的位置,隻有在一些特殊的場景下,才能适用 volatile。總的來說,必須同時滿足下面兩個條件才能保證在并發環境的線程安全:

(1)對變量的寫操作不依賴于目前值(比如 i++),或者說是單純的變量指派(booleanflag = true)。

(2)該變量沒有包含在具有其他變量的不變式中,也就是說,不同的 volatile 變量之間,不能互相依賴。隻有在狀态真正獨立于程式内其他内容時才能使用 volatile。

9.先行發生原則

1、程式次序規則。在一個線程内,書寫在前面的代碼先行發生于後面的。确切地說應該是,按照程式的控制流順序,因為存在一些分支結構。

2、Volatile變量規則。對一個volatile修飾的變量,對他的寫操作先行發生于讀操作。

3、線程啟動規則。Thread對象的start()方法先行發生于此線程的每一個動作。

4、線程終止規則。線程的所有操作都先行發生于對此線程的終止檢測。

5、線程中斷規則。對線程interrupt()方法的調用先行發生于被中斷線程的代碼所檢測到的中斷事件。

6、對象終止規則。一個對象的初始化完成(構造函數之行結束)先行發生于發的finilize()方法的開始。

10.程序和線程之間的調用算法

11.java中常見的鎖

12.synchronized底層實作原理

13.synchronized和ReentrantLock差別是什麼?

  • synchronized 競争鎖時會一直等待;ReentrantLock 可以嘗試擷取鎖,并得到擷取結果
  • synchronized 擷取鎖無法設定逾時;ReentrantLock 可以設定擷取鎖的逾時時間
  • synchronized 無法實作公平鎖;ReentrantLock 可以滿足公平鎖,即先等待先擷取到鎖
  • synchronized 控制等待和喚醒需要結合加鎖對象的 wait() 和 notify()、notifyAll();ReentrantLock 控制等待和喚醒需要結合 Condition 的 await() 和 signal()、signalAll() 方法
  • synchronized 是 JVM 層面實作的;ReentrantLock 是 JDK 代碼層面實作
  • synchronized 在加鎖代碼塊執行完或者出現異常,自動釋放鎖;ReentrantLock 不會自動釋放鎖,需要在 finally{} 代碼塊顯示釋放

14.ReentrantReadWriteLock讀寫鎖詳解

15.BlockingQueue阻塞隊列的實作方式