多線程常見面試題總結


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()方法的開始。

7、傳遞性。A先行發生B,B先行發生C,那么,A先行發生C。

8、管程鎖定規則。一個unlock操作先行發生於后面對同一個鎖的lock操作。

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阻塞隊列的實現方式

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM