一、什么是線程和進程?
進程:
是程序的一次執行過程,是系統運行程序的基本單元(就比如打開某個應用,就是開啟了一個進程),因此進程是動態的。系統運行一個程序即是一個程序從創建、運行到消亡的過程。
在 Java 中,當我們啟動 main 函數時其實就是啟動了 JVM 進程,而 main 函數所在的線程就是這個進程中的一個線程,也稱主線程。
線程:
線程與就進程相似,但線程是一個比進程更小的執行單位。一個進程在執行過程中可以產生多個線程。與進程不同的是同類的多個線程共享進程的堆和方法區資源,但每個線程有自己的程序計數器、虛擬機棧和本地方法棧,所以系統在產生一個進程,或是在各個進程之間做切換工作時,負擔要比進程小得多,也正因為如此,線程也被稱為輕量級進程。
二、線程與進程的關系,區別及優缺點?
從 JVM 角度說進程和線程之間的關系
下圖是 Java 內存區域,通過下圖我們從 JVM 的角度說明線程與進程之間的關系。

可以看出,一個進程可以有多個線程,多個線程共享進程的堆和方法區(JDK 1.8 之后的元空間)資源。但是每個線程有自己的程序計數器、虛擬機棧和本地方法棧。
綜上:線程是進程划分成的更小的運行單位。線程與進程最大的不同在於基本上各進程是獨立的,而各線程則不一定,因為同一進程中的線程極有可能會相互影響。線程執行開銷小,但不利於資源的管理和保護;而進程則相反。
為什么程序計數器、虛擬機棧和本地方法棧是線程私有的呢?為什么堆和方法區是線程共享的呢?
(1) 程序計數器為什么是私有的?
首先明確程序計數器的作用:
- 字節碼解釋器通過改變程序計數器來一次讀取指令,從而實現代碼的流程控制。如:順序執行、選擇、循環、異常處理。
- 在多線程的情況下,程序計數器用於記錄當前線程執行的位置,從而當線程被切換回來的時候能夠知道該線程運行到哪了。
需要注意的是:如果執行的是 native 方法,那么程序計數器記錄的是 undefined 地址,只有執行的是 Java 代碼時程序計數器記錄的才是下一條指令的地址。
所以,程序計數器私有主要是為了線程切換后能夠恢復到正確的執行位置。
(2) 虛擬機棧和本地方法棧為什么是私有的?
- 虛擬機棧:每個Java 方法在執行的同時會創建一個幀棧用於存儲局部變量表、操作數棧、常量池引用等信息。從方法調用直至完成的過程,就對應一個幀棧在 Java 虛擬機中入棧和出棧的過程。
- 本地方法棧:和虛擬機的作用非常相似。區別是:虛擬機為虛擬機執行 Java 方法(也就是字節碼)服務,而本地方法棧則為虛擬機使用到的 native 方法服務。在 HotSpot 虛擬機中和 Java 虛擬機棧合二為一。
所以,為了保證線程中的局部變量不被別的線程訪問到,虛擬機棧和本地方法棧是線程私有的。
(3) 堆和方法區
堆和方法區是所有線程共享的資源,其中堆是進程中最大的一塊內存,主要用來存放新創建的對象(所有的對象都在這里分配內存);方法區主要用於存放已被加載的類信息、常量、靜態變量、即時編譯器編譯后的代碼數據等。
參考:JavaGuide 公眾號及其相應的 Github
三、並發和並行有什么區別?
- 並發:同一時間段,多個任務都在執行(單位時間內不一定同時執行);
- 並行:單位時間內,多個任務同時執行。
並發的關鍵是你有處理多個任務的能力,不一定要同時。 而並行的關鍵是你有同時處理多個任務的能力。
四、為什么要使用多線程?
先總體上:
- 從計算機底層來說:線程可以比作是輕量級的進程,是程序執行的最小單元,線程間的切換和調度的成本遠遠小於進程。另外,多核 CPU 時代意味着多個線程可以同時運行,這減少了線程上下文切換的開銷。
- 從當代互聯網發展趨勢來說:現在的系統動不動就要求百萬級甚至千萬級的並發量,而多線程並發編程正式開發高並發系統的基礎,利用好多線程機制可以大大提高系統的並發能力以及性能。
再深入到計算機底層:
- 單核時代:在單核時代多線程主要是為了提高 CPU 和 IO 設備的綜合利用率。
- 多核時代:多核時代主要是為了提高 CPU 的利用率。
五、使用多線程可能會帶來什么問題?
並發編程的目的就是為了能提高程序的執行效率提高程序運行速度,但是並發編程並不總是能提高程序運行速度的,而並發編程可能會遇到很多問題,比如:內存泄漏、上下文切換、死鎖等,還有受限於硬件和軟件和資源閑置問題。
六、說說線程的生命周期和狀態。
Java 線程在運行的生命周期中的指定時刻只可能指定處於下面幾種不同狀態的其中一個狀態:
- 新建狀態(NEW):新創建了一個線程對象;
- 就緒狀態(RUNNABLE):線程創建后,其他線程調用了該對象的 start() 方法。該方法狀態的線程位於可運行線程池中,變得可運行,等待獲取 CPU 的使用權;
- 運行狀態(RUNNING):就緒狀態的線程獲取了 CPU,執行程序代碼;
- 阻塞狀態(BLOCKED):阻塞狀態是線程因為某種原因放棄 CPU 使用權,暫時停止運行。知道線程進入就緒狀態,才有機會轉到運行狀態。阻塞的情況分為三種:
- 等待阻塞:運行的線程執行 wait() 方法,JVM 會把該線程放入線程池中。
- 同步阻塞:運行的線程在獲取對象的同步鎖時,若該同步鎖被別的線程占用,則 JVM 會把該線程放入鎖池中。
- 其他阻塞:運行的線程執行 sleep() 或 join() 方法,或者發出了 I/O 請求時,JVM 會把該線程設置為阻塞狀態。當 sleep() 超時、join() 等待線程終止或者超時、或者 I/O 處理完畢時,線程重新轉入就緒狀態。
- 死亡狀態(DEAD):線程執行完了或者因異常退出了 run() 方法,該線程結束生命周期。
線程在生命周期中並不是固定處於一個狀態,而是隨着代碼的執行在不同狀態之間切換。Java 線程狀態變遷如下圖(圖為《Java 並發編程的藝術》)

可以看出:線程創建之初處於 NEW (新建) 狀態。調用 start() 方法后開始運行,線程這時候處於 READY (可運行) 狀態。可運行狀態的線程獲得了 CPU 時間片 (timeslice) 后就處於 RUNNING (運行)狀態。線程執行了 wait() 方法后,線程進入 WAITING (超時等待) 狀態相當於等待狀態的基礎上增加了超時限制,比如 sleep(long millis) 方法或 waiting(long millis) 方法可以將 Java 線程置於 TIME WAITING 狀態。當超時時間達到后 Java 線程將會返回到 RUNNABLE 狀態。當線程調用同步方法時,在沒有獲取到鎖的情況下,線程將會進入到 BLOCKED (阻塞)狀態。線程在執行 Runnable 的 run() 方法之后將進入到 TERMINATED (終止) 狀態。
七、java 中如何創建線程?
Java 中創建線程有四種方式:① 繼承 Thread;② 實現 Runnable 接口;③ 線程池;④ 實現 Callable 接口。
關於 Thread 或者 Runnable 接口,首先 Runnable 是接口,實現了改接口的類還可以繼承其他類,更靈活;其次,Runnable 任務可以在 Executors 中或者 ExecutorService 提交運行。
Future 和 Callable:Callable 與 Runnable 一樣都是代表抽象的計算任務,其中的 call 方法做用與 run 一樣,但是會返回一個值。Future 表示一個任務的生命周期,並提供了相應的方法來判斷是否已經完成或者取消,以及獲取任務的結果。ExecutorService 中所有的 submit 方法都會返回一個 future。
Callable 和 Runnable 的區別:
- Callable 定義的方法是 call,而 Runnable 定義的方法是 run;
- Callable 的 call 方法可以有返回值,而 Runnable 的 run 方法不能有返回值;
- Callable 的 call 方法可以拋出異常,而 Runnable 的 run 方法不能拋出異常。
八、什么是上下文切換?
多線程編程中一般線程的個數都大於 CPU 核的個數,而一個 CPU 核在任意時刻只能被一個線程使用,為了讓這些縣城都能得到有效執行,CPU 采取的策略是為每個線程分配時間片並輪轉的形式。當一個線程是時間片用完的時候就會重新處於就緒狀態讓給其他線程使用,這個過程就屬於一次上下文切換。也就是:當任務執行完, CPU 時間片切換到另一個任務之前會先保存自己的狀態,以便於再切換回這個任務時,可以加載這個任務的狀態。任務從保持到再加載的過程就是一個上下文切換。
上下文切換通常是計算密集型的。也就是說,它需要相當可觀的處理器時間,在每秒幾十上百次的切換中,每次切換都需要納秒量級的時間。所以,上下文切換對系統來說意味着消耗大量的 CPU 時間,事實上,可能是操作系統中時間消耗最大的操作。
Linux 相比與其他操作系統(包括其他類 Unix 系統)有很多的優點,其中有一項就是,其上下文切換和模式切換的時間消耗非常少。
九、什么是線程死鎖?怎么避免?
死鎖:
兩個或者兩個以上的線程在執行的過程中,因爭奪資源產生的一種互相等待的現象。
為什么會出現死鎖?
Java 運行多線程並發控制,當多個線程同時操作一個共享的資源變量時(如數據的增刪改查),將會導致數據出現不正確的結果,相互之間產生沖突,因此加入鎖保證了該變量的唯一性和准確性。
如下代碼(代碼源自《Java多線程編程核心技術》):
public class DeadThreadDemo implements Runnable{ public String username; public Object lock1 = new Object(); public Object lock2 = new Object(); public void setFlag(String username) { this.username = username; } @Override public void run(){ if(username.equals("a")) { synchronized (lock1) { try { System.out.println("username = " + username); Thread.sleep(3000); }catch(InterruptedException e) { e.printStackTrace(); } synchronized (lock2) { System.out.println("按 lock1->lock2代碼 順序執行了"); } } } if(username.equals("b")) { synchronized (lock2) { try { System.out.println("username = " + username); Thread.sleep(3000); }catch(InterruptedException e) { e.printStackTrace(); } synchronized (lock1) { System.out.println("按lock2->lock1代碼順序執行了"); } } } } }
測試類:
public class DeadThreadTest { public static void main(String[] args) { try { DeadThreadDemo dtd1 = new DeadThreadDemo(); dtd1.setFlag("a"); Thread thread1 = new Thread(dtd1); thread1.start(); Thread.sleep(100); dtd1.setFlag("b"); Thread thread2 = new Thread(dtd1); thread2.start(); }catch(InterruptedException e) { e.printStackTrace(); } } }
輸出:
username = a username = b
線程 a 通過 synchronized (lock1) 獲得 lock1 的監視器鎖,然后通過thread.sleap(3000); 讓線程 a 休眠 3s 為的是讓線程 b 得到執行然后獲取到 lock2 的監視器鎖。線程 a 和線程 b 休眠結束了都開始企圖請求獲取對方的資源,然后這兩個線程就會陷入互相等待的狀態,這也就產生了死鎖。上面的例子符合產生死鎖的四個必要條件。
死鎖產生的四個條件:
- 互斥條件: 該資源任意一個時刻只由一個線程占用;
- 請求與保持條件:一個線程因請求資源而阻塞,對已獲得的資源保持不放;
- 不剝奪條件:線程已經獲得的資源在未使用完之前不能被其他線程強行剝奪,只由自己使用完畢后才釋放資源;
- 循環等待條件:若干線程之間形成一種頭尾相接的循環等待資源關系。
怎么避免線程死鎖?
只需要破壞產生死鎖的四個條件之一即可。
- 破壞互斥條件:這個條件我們沒有辦法破壞,因為我們用鎖本身就是想讓他們互斥的(臨界資源需要互斥訪問)。
- 破壞請求與保持條件:一次性申請所有的資源
- 破壞不剝奪條件:占用部分資源的線程進一步申請其他資源時,如果申請不到,可以主動釋放它占有的資源。
- 破壞循環等待條件:靠按順序申請資源來預防。按照某一順序申請資源,釋放資源則反序釋放。破壞循環等待條件。
十、sleep() 方法和 wait() 方法區別和共同點
- 兩者最主要的區別在於:sleep() 方法沒有釋放鎖,而 wait() 方法釋放了鎖;
- 兩者都可以暫停多線程;
- wait() 通常被用於線程間交互/通信,sleep() 通常被用於暫停執行;
- wait() 方法被調用后,線程不會自動蘇醒,需要別的線程調用同一個對象上的 notify() 或者 notifyAll() 方法。sleep 執行完后,會自動蘇醒。
十一、為什么我們調用 start() 方法時會執行 run() 方法,為什么我們不能直接調用 run() 方法?
new 一個 Thread,線程進入了新建狀態;調用 start() 方法,會啟動一個線程並使線程進入就緒狀態,當分配到時間片后就可以開始運行了。start() 會執行線程的相應准備工作,然后自動執行 run() 方法的內容,這是真正的多線程工作。而直接執行 run() 方法,會把 run() 方法當成一個 main 線程下的普通方法去執行,並不會在某個線程中執行它,所以這不是多線程工作。
總之:調用 start() 方法可啟動線程並使線程進入就緒狀態,而 run() 方法只是 thread 的一個普通方法,還是在主線程里執行的。
一個材料人跨行到互聯網的研究僧
希望大家能多多關注~

