本文爲 SnailClimb 的原創,目前已經收錄自我開源的 JavaGuide 中(61.5 k Star!【Java學習+面試指南】 一份涵蓋大部分Java程序員所須要掌握的核心知識。以爲內容不錯再 Star!)。java
另外推薦一篇原創:終極推薦!多是最適合你的Java學習路線+方法+網站+書籍推薦!git
進程是程序的一次執行過程,是系統運行程序的基本單位,所以進程是動態的。系統運行一個程序便是一個進程從建立,運行到消亡的過程。程序員
在 Java 中,當咱們啓動 main 函數時其實就是啓動了一個 JVM 的進程,而 main 函數所在的線程就是這個進程中的一個線程,也稱主線程。github
以下圖所示,在 windows 中經過查看任務管理器的方式,咱們就能夠清楚看到 window 當前運行的進程(.exe 文件的運行)。面試
線程與進程類似,但線程是一個比進程更小的執行單位。一個進程在其執行的過程當中能夠產生多個線程。與進程不一樣的是同類的多個線程共享進程的堆和方法區資源,但每一個線程有本身的程序計數器、虛擬機棧和本地方法棧,因此係統在產生一個線程,或是在各個線程之間做切換工做時,負擔要比進程小得多,也正由於如此,線程也被稱爲輕量級進程。spring
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 方法便可):windows
[5] Attach Listener //添加事件
[4] Signal Dispatcher // 分發處理給 JVM 信號的線程
[3] Finalizer //調用對象 finalize 方法的線程
[2] Reference Handler //清除 reference 線程
[1] main //main 線程,程序入口
複製代碼
從上面的輸出內容能夠看出:一個 Java 程序的運行是 main 線程和多個其餘線程同時運行。後端
從 JVM 角度說進程和線程之間的關係springboot
下圖是 Java 內存區域,經過下圖咱們從 JVM 的角度來講一下線程和進程之間的關係。若是你對 Java 內存區域 (運行時數據區) 這部分知識不太瞭解的話能夠閱讀一下這篇文章:《多是把 Java 內存區域講的最清楚的一篇文章》
從上圖能夠看出:一個進程中能夠有多個線程,多個線程共享進程的堆和方法區 (JDK1.8 以後的元空間)資源,可是每一個線程有本身的程序計數器、虛擬機棧 和 本地方法棧。
總結: 線程 是 進程 劃分紅的更小的運行單位。線程和進程最大的不一樣在於基本上各進程是獨立的,而各線程則不必定,由於同一進程中的線程極有可能會相互影響。線程執行開銷小,但不利於資源的管理和保護;而進程正相反
下面是該知識點的擴展內容!
下面來思考這樣一個問題:爲何程序計數器、虛擬機棧和本地方法棧是線程私有的呢?爲何堆和方法區是線程共享的呢?
程序計數器主要有下面兩個做用:
須要注意的是,若是執行的是 native 方法,那麼程序計數器記錄的是 undefined 地址,只有執行的是 Java 代碼時程序計數器記錄的纔是下一條指令的地址。
因此,程序計數器私有主要是爲了線程切換後能恢復到正確的執行位置。
因此,爲了保證線程中的局部變量不被別的線程訪問到,虛擬機棧和本地方法棧是線程私有的。
堆和方法區是全部線程共享的資源,其中堆是進程中最大的一塊內存,主要用於存放新建立的對象 (全部對象都在這裏分配內存),方法區主要用於存放已被加載的類信息、常量、靜態變量、即時編譯器編譯後的代碼等數據。
先從整體上來講:
再深刻到計算機底層來探討:
併發編程的目的就是爲了能提升程序的執行效率提升程序運行速度,可是併發編程並不老是能提升程序運行速度的,並且併發編程可能會遇到不少問題,好比:內存泄漏、上下文切換、死鎖還有受限於硬件和軟件的資源閒置問題。
Java 線程在運行的生命週期中的指定時刻只可能處於下面 6 種不一樣狀態的其中一個狀態(圖源《Java 併發編程藝術》4.1.4 節)。
線程在生命週期中並非固定處於某一個狀態而是隨着代碼的執行在不一樣狀態之間切換。Java 線程狀態變遷以下圖所示(圖源《Java 併發編程藝術》4.1.4 節):
由上圖能夠看出:線程建立以後它將處於 NEW(新建) 狀態,調用 start()
方法後開始運行,線程這時候處於 READY(可運行) 狀態。可運行狀態的線程得到了 CPU 時間片(timeslice)後就處於 RUNNING(運行) 狀態。
操做系統隱藏 Java 虛擬機(JVM)中的 RUNNABLE 和 RUNNING 狀態,它只能看到 RUNNABLE 狀態(圖源:HowToDoInJava:Java Thread Life Cycle and Thread States),因此 Java 系統通常將這兩個狀態統稱爲 RUNNABLE(運行中) 狀態 。
當線程執行 wait()
方法以後,線程進入 WAITING(等待) 狀態。進入等待狀態的線程須要依靠其餘線程的通知纔可以返回到運行狀態,而 TIME_WAITING(超時等待) 狀態至關於在等待狀態的基礎上增長了超時限制,好比經過 sleep(long millis)
方法或 wait(long millis)
方法能夠將 Java 線程置於 TIMED WAITING 狀態。當超時時間到達後 Java 線程將會返回到 RUNNABLE 狀態。當線程調用同步方法時,在沒有獲取到鎖的狀況下,線程將會進入到 BLOCKED(阻塞) 狀態。線程在執行 Runnable 的run()
方法以後將會進入到 TERMINATED(終止) 狀態。
多線程編程中通常線程的個數都大於 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 休眠結束了都開始企圖請求獲取對方的資源,而後這兩個線程就會陷入互相等待的狀態,這也就產生了死鎖。上面的例子符合產生死鎖的四個必要條件。
學過操做系統的朋友都知道產生死鎖必須具有如下四個條件:
咱們只要破壞產生死鎖的四個條件中的其中一個就能夠了。
破壞互斥條件
這個條件咱們沒有辦法破壞,由於咱們用鎖原本就是想讓他們互斥的(臨界資源須要互斥訪問)。
破壞請求與保持條件
一次性申請全部的資源。
破壞不剝奪條件
佔用部分資源的線程進一步申請其餘資源時,若是申請不到,能夠主動釋放它佔有的資源。
破壞循環等待條件
靠按序申請資源來預防。按某一順序申請資源,釋放資源則反序釋放。破壞循環等待條件。
咱們對線程 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 獲取到就能夠執行了。這樣就破壞了破壞循環等待條件,所以避免了死鎖。
這是另外一個很是經典的 java 多線程面試問題,並且在面試中會常常被問到。很簡單,可是不少人都會答不上來!
new 一個 Thread,線程進入了新建狀態;調用 start() 方法,會啓動一個線程並使線程進入了就緒狀態,當分配到時間片後就能夠開始運行了。 start() 會執行線程的相應準備工做,而後自動執行 run() 方法的內容,這是真正的多線程工做。 而直接執行 run() 方法,會把 run 方法當成一個 main 線程下的普通方法去執行,並不會在某個線程中執行它,因此這並非多線程工做。
總結: 調用 start 方法方可啓動線程並使線程進入就緒狀態,而 run 方法只是 thread 的一個普通方法調用,仍是在主線程裏執行。
做者的其餘開源項目推薦: