1. 程式人生 > 其它 >Java 併發基礎常見面試題總結

Java 併發基礎常見面試題總結

1. 什麼是執行緒和程序?

1.1. 何為程序?

程序是程式的一次執行過程,是系統執行程式的基本單位,因此程序是動態的。系統執行一個程式即是一個程序從建立,執行到消亡的過程。

在 Java 中,當我們啟動 main 函式時其實就是啟動了一個 JVM 的程序,而 main 函式所在的執行緒就是這個程序中的一個執行緒,也稱主執行緒。

如下圖所示,在 windows 中通過檢視工作管理員的方式,我們就可以清楚看到 window 當前執行的程序(.exe 檔案的執行)。

1.2. 何為執行緒?

執行緒與程序相似,但執行緒是一個比程序更小的執行單位。一個程序在其執行的過程中可以產生多個執行緒。與程序不同的是同類的多個執行緒共享程序的

方法區資源,但每個執行緒有自己的程式計數器虛擬機器棧本地方法棧,所以系統在產生一個執行緒,或是在各個執行緒之間作切換工作時,負擔要比程序小得多,也正因為如此,執行緒也被稱為輕量級程序。

Java 程式天生就是多執行緒程式,我們可以通過 JMX 來看一下一個普通的 Java 程式有哪些執行緒,程式碼如下。

public class MultiThread {
    public static void main(String[] args) {
        // 獲取 Java 執行緒管理 MXBean
    ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
        
// 不需要獲取同步的 monitor 和 synchronizer 資訊,僅獲取執行緒和執行緒堆疊資訊 ThreadInfo[] threadInfos = threadMXBean.dumpAllThreads(false, false); // 遍歷執行緒資訊,僅列印執行緒 ID 和執行緒名稱資訊 for (ThreadInfo threadInfo : threadInfos) { System.out.println("[" + threadInfo.getThreadId() + "] " + threadInfo.getThreadName()); } } }

上述程式輸出如下(輸出內容可能不同,不用太糾結下面每個執行緒的作用,只用知道 main 執行緒執行 main 方法即可):

[5] Attach Listener //新增事件
[4] Signal Dispatcher // 分發處理給 JVM 訊號的執行緒
[3] Finalizer //呼叫物件 finalize 方法的執行緒
[2] Reference Handler //清除 reference 執行緒
[1] main //main 執行緒,程式入口

從上面的輸出內容可以看出:一個 Java 程式的執行是 main 執行緒和多個其他執行緒同時執行

2. 請簡要描述執行緒與程序的關係,區別及優缺點?

從 JVM 角度說程序和執行緒之間的關係

2.1. 圖解程序和執行緒的關係

下圖是 Java 記憶體區域,通過下圖我們從 JVM 的角度來說一下執行緒和程序之間的關係。如果你對 Java 記憶體區域 (執行時資料區) 這部分知識不太瞭解的話可以閱讀一下這篇文章:《可能是把 Java 記憶體區域講的最清楚的一篇文章》

從上圖可以看出:一個程序中可以有多個執行緒,多個執行緒共享程序的方法區 (JDK1.8 之後的元空間)資源,但是每個執行緒有自己的程式計數器虛擬機器棧本地方法棧

總結: 執行緒是程序劃分成的更小的執行單位。執行緒和程序最大的不同在於基本上各程序是獨立的,而各執行緒則不一定,因為同一程序中的執行緒極有可能會相互影響。執行緒執行開銷小,但不利於資源的管理和保護;而程序正相反。

下面是該知識點的擴充套件內容!

下面來思考這樣一個問題:為什麼程式計數器虛擬機器棧本地方法棧是執行緒私有的呢?為什麼堆和方法區是執行緒共享的呢?

2.2. 程式計數器為什麼是私有的?

程式計數器主要有下面兩個作用:

  1. 位元組碼直譯器通過改變程式計數器來依次讀取指令,從而實現程式碼的流程控制,如:順序執行、選擇、迴圈、異常處理。

  2. 在多執行緒的情況下,程式計數器用於記錄當前執行緒執行的位置,從而當執行緒被切換回來的時候能夠知道該執行緒上次執行到哪兒了。

需要注意的是,如果執行的是 native 方法,那麼程式計數器記錄的是 undefined 地址,只有執行的是 Java 程式碼時程式計數器記錄的才是下一條指令的地址。

所以,程式計數器私有主要是為了執行緒切換後能恢復到正確的執行位置

2.3. 虛擬機器棧和本地方法棧為什麼是私有的?

  • 虛擬機器棧: 每個 Java 方法在執行的同時會建立一個棧幀用於儲存區域性變量表、運算元棧、常量池引用等資訊。從方法呼叫直至執行完成的過程,就對應著一個棧幀在 Java 虛擬機器棧中入棧和出棧的過程。

  • 本地方法棧: 和虛擬機器棧所發揮的作用非常相似,區別是: 虛擬機器棧為虛擬機器執行 Java 方法 (也就是位元組碼)服務,而本地方法棧則為虛擬機器使用到的 Native 方法服務。 在 HotSpot 虛擬機器中和 Java 虛擬機器棧合二為一。

所以,為了保證執行緒中的區域性變數不被別的執行緒訪問到,虛擬機器棧和本地方法棧是執行緒私有的。

2.4. 一句話簡單瞭解堆和方法區

堆和方法區是所有執行緒共享的資源,其中堆是程序中最大的一塊記憶體,主要用於存放新建立的物件 (幾乎所有物件都在這裡分配記憶體),方法區主要用於存放已被載入的類資訊、常量、靜態變數、即時編譯器編譯後的程式碼等資料。

3. 說說併發與並行的區別?

  • 併發: 同一時間段,多個任務都在執行 (單位時間內不一定同時執行);

  • 並行: 單位時間內,多個任務同時執行。

4. 為什麼要使用多執行緒呢?

先從總體上來說:

  • 從計算機底層來說: 執行緒可以比作是輕量級的程序,是程式執行的最小單位,執行緒間的切換和排程的成本遠遠小於程序。另外,多核 CPU 時代意味著多個執行緒可以同時執行,這減少了執行緒上下文切換的開銷。

  • 從當代網際網路發展趨勢來說: 現在的系統動不動就要求百萬級甚至千萬級的併發量,而多執行緒併發程式設計正是開發高併發系統的基礎,利用好多執行緒機制可以大大提高系統整體的併發能力以及效能。

再深入到計算機底層來探討:

  • 單核時代: 在單核時代多執行緒主要是為了提高單程序利用 CPU 和 IO 系統的效率。 假設只運行了一個 Java 程序的情況,當我們請求 IO 的時候,如果 Java 程序中只有一個執行緒,此執行緒被 IO 阻塞則整個程序被阻塞。CPU 和 IO 裝置只有一個在執行,那麼可以簡單地說系統整體效率只有 50%。當使用多執行緒的時候,一個執行緒被 IO 阻塞,其他執行緒還可以繼續使用 CPU。從而提高了 Java 程序利用系統資源的整體效率。

  • 多核時代: 多核時代多執行緒主要是為了提高程序利用多核 CPU 的能力。舉個例子:假如我們要計算一個複雜的任務,我們只用一個執行緒的話,不論系統有幾個 CPU 核心,都只會有一個 CPU 核心被利用到。而建立多個執行緒,這些執行緒可以被對映到底層多個 CPU 上執行,在任務中的多個執行緒沒有資源競爭的情況下,任務執行的效率會有顯著性的提高,約等於(單核時執行時間/CPU 核心數)。

5. 使用多執行緒可能帶來什麼問題?

併發程式設計的目的就是為了能提高程式的執行效率提高程式執行速度,但是併發程式設計並不總是能提高程式執行速度的,而且併發程式設計可能會遇到很多問題,比如:記憶體洩漏、死鎖、執行緒不安全等等。

6. 說說執行緒的生命週期和狀態?

Java 執行緒在執行的生命週期中的指定時刻只可能處於下面 6 種不同狀態的其中一個狀態(圖源《Java 併發程式設計藝術》4.1.4 節)。

執行緒在生命週期中並不是固定處於某一個狀態而是隨著程式碼的執行在不同狀態之間切換。Java 執行緒狀態變遷如下圖所示(圖源《Java 併發程式設計藝術》4.1.4 節):

由上圖可以看出:執行緒建立之後它將處於 NEW(新建) 狀態,呼叫 start() 方法後開始執行,執行緒這時候處於 READY(可執行) 狀態。可執行狀態的執行緒獲得了 CPU 時間片(timeslice)後就處於 RUNNING(執行) 狀態。

在作業系統中層面執行緒有 READY 和 RUNNING 狀態,而在 JVM 層面只能看到 RUNNABLE 狀態(圖源:HowToDoInJavaJava Thread Life Cycle and Thread States),所以 Java 系統一般將這兩個狀態統稱為 RUNNABLE(執行中) 狀態 。

為什麼 JVM 沒有區分這兩種狀態呢? (摘自:java執行緒執行怎麼有第六種狀態? - Dawell的回答 ) 現在的<b>時分</b>(time-sharing)<b>多工</b>(multi-task)作業系統架構通常都是用所謂的“<b>時間分片</b>(time quantum or time slice)”方式進行<b>搶佔式</b>(preemptive)輪轉排程(round-robin式)。這個時間分片通常是很小的,一個執行緒一次最多隻能在 CPU 上執行比如 10-20ms 的時間(此時處於 running 狀態),也即大概只有 0.01 秒這一量級,時間片用後就要被切換下來放入排程佇列的末尾等待再次排程。(也即回到 ready 狀態)。執行緒切換的如此之快,區分這兩種狀態就沒什麼意義了。

當執行緒執行 wait()方法之後,執行緒進入 WAITING(等待) 狀態。進入等待狀態的執行緒需要依靠其他執行緒的通知才能夠返回到執行狀態,而 TIME_WAITING(超時等待) 狀態相當於在等待狀態的基礎上增加了超時限制,比如通過 sleep(long millis)方法或 wait(long millis)方法可以將 Java 執行緒置於 TIMED WAITING 狀態。當超時時間到達後 Java 執行緒將會返回到 RUNNABLE 狀態。當執行緒呼叫同步方法時,在沒有獲取到鎖的情況下,執行緒將會進入到 BLOCKED(阻塞) 狀態。執行緒在執行 Runnable 的run()方法之後將會進入到 TERMINATED(終止) 狀態。