剖析面試最常見問題之 Java 併發基礎知識

剖析面試最常見問題之 Java 併發基礎知識

點關注,不迷路;持續更新Java相關技術及資訊!!!

什麼是線程和進程?

何爲進程?
進程是程序的一次執行過程,是系統運行程序的基本單位,因此進程是動態的。系統運行一個程序即是一個進程從創建,運行到消亡的過程。

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

如下圖所示,在 windows 中通過查看任務管理器的方式,我們就可以清楚看到 window 當前運行的進程(.exe 文件的運行)。
在這裏插入圖片描述
推薦:博主的一個【java技術交流小站】:點擊進入 暗號csdn

羣號:530720915
進羣驗證“csdn” 分享Java中高級開發進階資料、源碼、筆記、視頻、Dubbo、Redis、Netty、zookeeper、Springcloud、分佈式、高併發等架構技術架構教程

何爲線程?

線程與進程相似,但線程是一個比進程更小的執行單位。一個進程在其執行的過程中可以產生多個線程。與進程不同的是同類的多個線程共享進程的方法區資源,但每個線程有自己的程序計數器、虛擬機棧本地方法棧,所以系統在產生一個線程,或是在各個線程之間作切換工作時,負擔要比進程小得多,也正因爲如此,線程也被稱爲輕量級進程。

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 線程和多個其他線程同時運行

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

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

圖解進程和線程的關係
下圖是 Java 內存區域,通過下圖我們從 JVM 的角度來說一下線程和進程之間的關係。
在這裏插入圖片描述
從上圖可以看出:一個進程中可以有多個線程,多個線程共享進程的方法區 (JDK1.8 之後的元空間)資源,但是每個線程有自己的程序計數器虛擬機棧本地方法棧

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

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

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

程序計數器爲什麼是私有的?

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

  1. 字節碼解釋器通過改變程序計數器來依次讀取指令,從而實現代碼的流程控制,如:順序執行、選擇、循環、異常處理。
  2. 在多線程的情況下,程序計數器用於記錄當前線程執行的位置,從而當線程被切換回來的時候能夠知道該線程上次運行到哪兒了。

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

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

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

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

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

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

說說併發與並行的區別?

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

爲什麼要使用多線程呢?

先從總體上來說:

  • 從計算機底層來說:線程可以比作是輕量級的進程,是程序執行的最小單位,線程間的切換和調度的成本遠遠小於進程。另外,多核 CPU 時代意味着多個線程可以同時運行,這減少了線程上下文切換的開銷。
  • 從當代互聯網發展趨勢來說:現在的系統動不動就要求百萬級甚至千萬級的併發量,而多線程併發編程正是開發高併發系統的基礎,利用好多線程機制可以大大提高系統整體的併發能力以及性能。

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

  • 單核時代: 在單核時代多線程主要是爲了提高 CPU 和 IO 設備的綜合利用率。舉個例子:當只有一個線程的時候會導致 CPU 計算時,IO 設備空閒;進行 IO 操作時,CPU 空閒。我們可以簡單地說這兩者的利用率目前都是
    50%左右。但是當有兩個線程的時候就不一樣了,當一個線程執行 CPU 計算時,另外一個線程可以進行 IO
    操作,這樣兩個的利用率就可以在理想情況下達到 100%了。
  • 多核時代: 多核時代多線程主要是爲了提高 CPU 利用率。舉個例子:假如我們要計算一個複雜的任務,我們只用一個線程的話,CPU 只會一個 CPU 核心被利用到,而創建多個線程就可以讓多個 CPU 核心被利用到,這樣就提高了 CPU 的利用率。

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

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

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

Java 線程在運行的生命週期中的指定時刻只可能處於下面 6 種不同狀態的其中一個狀態
在這裏插入圖片描述
線程在生命週期中並不是固定處於某一個狀態而是隨着代碼的執行在不同狀態之間切換。Java 線程狀態變遷如下圖所示在這裏插入圖片描述
由上圖可以看出:線程創建之後它將處於 NEW(新建) 狀態,調用 start() 方法後開始運行,線程這時候處於 READY(可運行) 狀態。可運行狀態的線程獲得了 CPU 時間片(timeslice)後就處於 RUNNING(運行) 狀態。

操作系統隱藏 Java 虛擬機(JVM)中的 RUNNABLE 和 RUNNING 狀態,它只能看到 RUNNABLE 狀態,所以 Java
系統一般將這兩個狀態統稱爲 RUNNABLE(運行中) 狀態 。

在這裏插入圖片描述

什麼是上下文切換?

多線程編程中一般線程的個數都大於 CPU 核心的個數,而一個 CPU 核心在任意時刻只能被一個線程使用,爲了讓這些線程都能得到有效執行,CPU 採取的策略是爲每個線程分配時間片並輪轉的形式。當一個線程的時間片用完的時候就會重新處於就緒狀態讓給其他線程使用,這個過程就屬於一次上下文切換。

概括來說就是:當前任務在執行完 CPU 時間片切換到另一個任務之前會先保存自己的狀態,以便下次再切換會這個任務時,可以再加載這個任務的狀態。任務從保存到再加載的過程就是一次上下文切換。

上下文切換通常是計算密集型的。也就是說,它需要相當可觀的處理器時間,在每秒幾十上百次的切換中,每次切換都需要納秒量級的時間。所以,上下文切換對系統來說意味着消耗大量的 CPU 時間,事實上,可能是操作系統中時間消耗最大的操作。

Linux 相比與其他操作系統(包括其他類 Unix 系統)有很多的優點,其中有一項就是,其上下文切換和模式切換的時間消耗非常少。

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

認識線程死鎖

多個線程同時被阻塞,它們中的一個或者全部都在等待某個資源被釋放。由於線程被無限期地阻塞,因此程序不可能正常終止。

如下圖所示,線程 A 持有資源 2,線程 B 持有資源 1,他們同時都想申請對方的資源,所以這兩個線程就會互相等待而進入死鎖狀態
在這裏插入圖片描述
下面通過一個例子來說明線程死鎖,代碼模擬了上圖的死鎖的情況:

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 休眠結束了都開始企圖請求獲取對方的資源,然後這兩個線程就會陷入互相等待的狀態,這也就產生了死鎖。上面的例子符合產生死鎖的四個必要條件。

產生死鎖必須具備以下四個條件:

  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 釋放了對 resource1、resource2 的監視器鎖的佔用,線程 2 獲取到就可以執行了。這樣就破壞了破壞循環等待條件,因此避免了死鎖。

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

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

爲什麼我們調用start()方法時會執行run()方法,爲什麼我們不能直接調用run()方法?

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

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

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

既然讀到了這裏 別忘了在下評論’學習了’ 哦 還有前文的福利感謝 希望本文對大家有所幫助,一起進步,喜歡點個贊 評論 點下關注唄 以後將不斷持續更新~

【java技術交流小站】:點擊進入 暗號csdn

下編主題:
剖析面試最常見問題之Java 併發知識進階(上)
記得點關注哦

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章