做爲高級開發你不得不懂的面試題之Java 併發常見面試題總結(一) 持續更新......

1. 什麼線程以及進程?

1.1 何爲進程?

  進程是程序的一次執行過程,是系統運行程序的基本單位,所以進程是動態的。系統運行一個程序便是一個進程從建立,運行到消亡的過程。java

  在 Java 中,當咱們啓動 main 函數時其實就是啓動了一個 JVM 的進程,而 main 函數所在的線程就是這個進程中的一個線程,也稱主線程。面試

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 線程spa

[1] main //main 線程,程序入口線程

從上面的輸出內容能夠看出:一個 Java 程序的運行是 main 線程和多個其餘線程同時運行。

2. 請簡要描述線程與進程的關係,區別及優缺點?

從 JVM 角度說進程和線程之間的關係

2.1 圖解進程和線程的關係

下圖是 Java 內存區域,經過下圖咱們從 JVM 的角度來講一下線程和進程之間的關係。

從上圖能夠看出:一個進程中能夠有多個線程,多個線程共享進程的堆和方法區 (JDK1.8 以後的元空間)資源,可是每一個線程有本身的程序計數器、虛擬機棧 和 本地方法棧。

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

下面是該知識點的擴展內容!

下面來思考這樣一個問題:爲何程序計數器、虛擬機棧和本地方法棧是線程私有的呢?爲何堆和方法區是線程共享的呢?

2.2 程序計數器爲何是私有的?

程序計數器主要有下面兩個做用:

  1. 字節碼解釋器經過改變程序計數器來依次讀取指令,從而實現代碼的流程控制,如:順序執行、選擇、循環、異常處理。
  2. 在多線程的狀況下,程序計數器用於記錄當前線程執行的位置,從而當線程被切換回來的時候可以知道該線程上次運行到哪兒了。 須要注意的是,若是執行的是 native 方法,那麼程序計數器記錄的是 undefined 地址,只有執行的是 Java 代碼時程序計數器記錄的纔是下一條指令的地址。

因此,程序計數器私有主要是爲了線程切換後能恢復到正確的執行位置。

2.3 虛擬機棧和本地方法棧爲何是私有的?

  • 虛擬機棧: 每一個Java方法在執行的同時會建立一個棧幀用於存儲局部變量表、操做數棧、常量池引用等信息。從方法調用直至執行完成的過程,就對應着一個棧幀在 Java 虛擬機棧中入棧和出棧的過程。
  • 本地方法棧:和虛擬機棧所發揮的做用很是類似,區別是: 虛擬機棧爲虛擬機執行 Java 方法 (也就是字節碼)服務,而本地方法棧則爲虛擬機使用到的 Native 方法服務。 在 HotSpot 虛擬機中和 Java 虛擬機棧合二爲一。

因此,爲了保證線程中的局部變量不被別的線程訪問到,虛擬機棧和本地方法棧是線程私有的。

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

  堆和方法區是全部線程共享的資源,其中堆是進程中最大的一塊內存,主要用於存放新建立的對象 (全部對象都在這裏分配內存),方法區主要用於存放已被加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。

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

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

4. 爲何要使用多線程呢?

先從整體上來講:

  • 從計算機底層來講: 線程能夠比做是輕量級的進程,是程序執行的最小單位,線程間的切換和調度的成本遠遠小於進程。另外,多核 CPU 時代意味着多個線程能夠同時運行,這減小了線程上下文切換的開銷。

  • 從當代互聯網發展趨勢來講: 如今的系統動不動就要求百萬級甚至千萬級的併發量,而多線程併發編程正是開發高併發系統的基礎,利用好多線程機制能夠大大提升系統總體的併發能力以及性能。 再深刻到計算機底層來探討:

  • 單核時代: 在單核時代多線程主要是爲了提升 CPU 和 IO 設備的綜合利用率。舉個例子:當只有一個線程的時候會致使 CPU 計算時,IO 設備空閒;進行 IO 操做時,CPU 空閒。咱們能夠簡單地說這二者的利用率目前都是 50%左右。可是當有兩個線程的時候就不同了,當一個線程執行 CPU 計算時,另一個線程能夠進行 IO 操做,這樣兩個的利用率就能夠在理想狀況下達到 100%了。

  • 多核時代: 多核時代多線程主要是爲了提升 CPU 利用率。舉個例子:假如咱們要計算一個複雜的任務,咱們只用一個線程的話,CPU 只會一個 CPU 核心被利用到,而建立多個線程就可讓多個 CPU 核心被利用到,這樣就提升了 CPU 的利用率。

5.使用多線程可能帶來什麼問題?

  併發編程的目的就是爲了能提升程序的執行效率提升程序運行速度,可是併發編程並不老是能提升程序運行速度的,並且併發編程可能會遇到不少問題,好比:內存泄漏、上下文切換、死鎖還有受限於硬件和軟件的資源閒置問題。

6.說說線程的生命週期和狀態?

線程生命週期圖:

由上圖能夠看出:

一、新建狀態(new): 一個線程剛剛建立出來,但還未執行start()方法。

二、可運行狀態(runnable):線程調用了該對象的start()方法後,線程位於可運行的線程池中,變得可運行,等待獲取cpu的使用權。

三、運行狀態(running): 可運行態的線程獲取到了cpu的使用權後,執行程序代碼就爲運行態。

四、等待狀態:處於運行態的線程能夠經過各類手段是運行態的線程等待或阻塞,當等待或阻塞結束,又能夠恢復到可運行態,線程等待有下面三種狀況:

4.一、超時等待狀態(timedwaiting): 運行中的線程經過調用sleep()或join()方法,或發出I/O請求時,JVM會把該線程掛起等待,當超時時間達到時或I/O操做完成時,線程從新進入到可運行態(注意:若是線程持有鎖,線程sleep的過程當中不會釋放鎖)。

4.二、普通等待狀態(waiting): 處於運行態的線程經過調用wait()方法,JVM會把該線程放入到等待池中(注意:若是線程持有鎖,線程wait的過程當中會釋放鎖)

4.三、阻塞等待狀態(blocked): 處於運行態的線程在獲取對象的同步鎖時,若是該同步鎖正被別的線程所佔用,則JVM會把該線程放入到鎖池中進行等待,直到獲取到鎖後,線程才又進入到可運行態。

五、終止狀態(terminated): 線程執行完或因爲異常而退出,該線程的生命週期結束。

7. 什麼是上下文切換?

  多線程編程中通常線程的個數都大於CPU核心的個數,而一個CPU核心在任意時刻只能被一個線程使用,爲了讓這些線程都能獲得有效執行,CPU採起的策略是爲每一個線程分配時間片並輪轉的形式。當一個線程的時間片用完的時候就會從新處於就緒狀態讓給其餘線程使用,這個過程就屬於一次上下文切換。
  歸納來講就是:當前任務在執行完CPU時間片切換到另外一個任務以前會先保存本身的狀態,以便下次再切換回這個任務時,能夠再加載這個任務的狀態。任務從保存到再加載的過程就是一次上下文切換。

8.什麼是線程死鎖?如何避免死鎖?

8.1 瞭解死鎖

  多個線程同時被阻塞,他們中的一個或者所有都在等待某個資源被釋放。因爲線程被無限期地阻塞,所以程序不可能正常終止,最終致使死鎖產生。

  以下圖所示,線程A持有資源2,線程B持有資源1,他們同時都想申請對方的資源,因此這兩個線程就會互相等待而進入死鎖狀態。

下面經過一個例子來講明線程死鎖,代碼模擬了上圖的死鎖狀況 (源於《併發編程之美》):

package com.MyMineBug.demoRun.test;

public class DeadLockDemo {
 
    private static Object resource1 = new Object();//資源 1
    private static Object resource2 = new Object();//資源 2
 
    public static void main(String[] args) {
        new Thread(() -> {
            synchronized (resource1) {
                System.out.println(Thread.currentThread() + "get resource1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource2");
                synchronized (resource2) {
                    System.out.println(Thread.currentThread() + "get resource2");
                }
            }
        }, "線程 1").start();
 
        new Thread(() -> {
            synchronized (resource2) {
                System.out.println(Thread.currentThread() + "get resource2");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource1");
                synchronized (resource1) {
                    System.out.println(Thread.currentThread() + "get resource1");
                }
            }
        }, "線程 2").start();
    }
}

複製代碼

Output:

Thread[線程 1,5,main]get resource1
Thread[線程 2,5,main]get resource2
Thread[線程 1,5,main]waiting get resource2
Thread[線程 2,5,main]waiting get resource1

複製代碼

線程 A 經過 synchronized(resource1)得到resource1的監視器鎖,而後執行Thread.sleep(1000); 讓線程 A 休眠 1s 是爲了讓線程 B獲得執行,而後獲取到resource2的監視器鎖。線程 A 和線程 B 休眠結束後,都開始企圖請求對方獲取到的資源,而後這兩個線程就會陷入互相等待的狀態,這也就產生了死鎖。上面的例子同時符合產生死鎖的四個必要條件:

互斥條件: 該資源任意一個時刻只由一個線程佔用;

請求與保持條件:一個進程因請求資源而阻塞時,對已得到的資源持有不釋放;

不剝奪條件:線程已得到的資源,在末使用完以前不能被其餘線程強行剝奪,只有本身使用完畢後才釋放資源;

循環等待條件:若干進程之間造成一種頭尾相接的循環等待資源關係。

8.2 如何避免死鎖?

只要任意破壞產生死鎖的四個條件中的其中一個就能夠了:

  1. 破壞互斥條件: 該條件沒有辦法破壞,由於用鎖的意義原本就是想讓他們互斥的(臨界資源須要互斥訪問);
  2. 破壞請求與保持條件:一次性申請全部的資源;
  3. 破壞不剝奪條件:佔用部分資源的線程進一步申請其餘資源時,若是申請不到,能夠主動釋放它佔有的資源;
  4. 破壞循環等待條件:靠按序申請資源來預防。按某一順序申請資源,釋放資源則反序釋放。

對線程 2 的代碼修改爲下面這樣,就不會產生死鎖了:

new Thread(() -> {
            synchronized (resource1) {
                System.out.println(Thread.currentThread() + "get resource1");
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                System.out.println(Thread.currentThread() + "waiting get resource2");
                synchronized (resource2) {
                    System.out.println(Thread.currentThread() + "get resource2");
                }
            }
        }, "線程 2").start();

複製代碼

Output:

Thread[線程 1,5,main]get resource1
Thread[線程 1,5,main]waiting get resource2
Thread[線程 1,5,main]get resource2
Thread[線程 2,5,main]get resource1
Thread[線程 2,5,main]waiting get resource2
Thread[線程 2,5,main]get resource2
 
Process finished with exit code 0

複製代碼

咱們分析一下上面的代碼爲何能避免死鎖的發生?

  線程 1 首先獲取獲得resource1的監視器鎖,這時候線程2就獲取不到了;而後線程 1 再去獲取 resource2的監視器鎖,能夠獲取到;再而後線程1釋放了對resource一、resource2 的監視器鎖的佔用,線程2獲取到就能夠執行了;這樣就破壞了循環等待條件,所以避免了死鎖。

9. 說說 sleep() 方法和 wait() 方法區別和共同點?

  • 二者最主要的區別在於:sleep方法沒有釋放鎖,而wait方法釋放了鎖。二者均可以暫停線程的執行。
  • Wait 一般被用於線程間交互/通訊,sleep 一般被用於暫停執行。
  • wait() 方法被調用後,線程不會自動甦醒,須要別的線程調用同一個對象上的 notify() 或者 notifyAll() 方法。
  • sleep() 方法執行完成後,線程會自動甦醒。或者可使用wait(long timeout)超時後線程會自動甦醒。

10. 爲何咱們調用 start() 方法時會執行 run() 方法,爲何咱們不能直接調用 run() 方法?

這是另外一個很是經典的 java多線程面試問題,並且在面試中會常常被問到。很簡單,可是不少人都會答不上來!

new 一個 Thread,線程進入了新建狀態;調用 start() 方法,會啓動一個線程並使線程進入了就緒狀態,當分配到時間片後就能夠開始運行了。 start() 會執行線程的相應準備工做,而後自動執行 run() 方法的內容,這是真正的多線程工做。 而直接執行 run() 方法,會把 run 方法當成一個 main 線程下的普通方法去執行,並不會在某個線程中執行它,因此這並非多線程工做。

總結: 調用 start 方法方可啓動線程並使線程進入就緒狀態,而 run 方法只是 thread 的一個普通方法調用,仍是在主線程裏執行。

  若是以爲還不錯,請點個贊!!!

  Share Technology And Love Life

相關文章
相關標籤/搜索