1. 程式人生 > >Java 執行緒基礎,從這篇開始

Java 執行緒基礎,從這篇開始

![](https://img2020.cnblogs.com/other/1850167/202006/1850167-20200630085542966-1311282910.png) > 執行緒作為作業系統中最少排程單位,在當前系統的執行環境中,一般都擁有多核處理器,為了更好的充分利用 CPU,掌握其正確使用方式,能更高效的使程式執行。同時,在 Java 面試中,也是極其重要的一個模組。 # 執行緒簡介 一個獨立執行的程式是一個程序,一個程序中可以包含一個或多個執行緒,每個執行緒都有屬於自己的一些屬性,如堆疊,計數器等等。同時,一個執行緒在一個時間點上只能執行在一個 CPU 處理器核心上,不同執行緒之間也可以訪問共享變數。執行緒在執行時,系統給每個執行緒分配一些 CPU 時間片,CPU 在時間片這段時間執行某個執行緒,當這個時間片執行完又跳轉至下一段時間片執行緒,CPU 在這些執行緒中進行高速切換,使得程式像是在同時進行多個執行緒操作。 # 執行緒的實現 實現執行緒常用的兩種方式:繼承 java.lang.Thread 類、實現 java.lang.Runnable 介面。 ## 繼承 Thread 類方式 通過例項化 java.lang.Thread 類獲得執行緒。建立 Thread 物件,一般使用繼承 Thread 類的方式,然後通過方法重寫覆蓋 Thread 的某些方法。 首先建立一個繼承 Thread 的子類。 ```java public class DemoThread extends Thread{ // 重寫 Thread 類中的 run 方法 @Override public void run() { // currentThread().getName() 獲取當前執行緒名稱 System.out.println("java.lang.Thread 建立的"+ currentThread().getName() +"執行緒"); } } ``` 上面程式碼 DemoThread 例項化的物件就代表一個執行緒,通過重寫 run 方法,在 run 方法中實現該執行緒的邏輯實現。 ```java public class Main { public static void main(String[] args) { // 例項化 DemoThread 得到新建立的執行緒例項 DemoThread thread = new DemoThread(); // 給建立的子執行緒命名 thread.setName("DemoThread 子執行緒"); // 啟動執行緒 thread.start(); // 通過主執行緒列印資訊 System.out.println("main 執行緒"); } } ``` 在程式執行的主執行緒中建立子執行緒,並且命名為`DemoThread 子執行緒`,在程式的最後列印主執行緒列印的資訊。呼叫執行緒必須呼叫`start()`方法,在呼叫此方法之前,子執行緒是不存在的,只有`start()`方法呼叫後,才是真正的建立了執行緒。 執行結果: ![](https://img2020.cnblogs.com/other/1850167/202006/1850167-20200630085543460-466417426.png) 從結果可以看到,由於在主執行緒中建立了一個子執行緒,子執行緒相對於主執行緒就相當於是一個非同步操作,所以列印結果就有可能main執行緒先於子執行緒執行列印操作。 ## 實現 Runnable 介面方式 由於 Java 是單繼承的特性,所以當建立執行緒的子類繼承了其他的類,就無法實現繼承操作。這時就可以通過實現 Runnable 介面,來實現執行緒建立的邏輯。 首先建立一個實現 Runnable 的類。 ```java public class DemoRunnable implements Runnable { // 實現 Runnable 中的 run 方法 @Override public void run() { System.out.println("java.lang.Runnable 建立的 "+ Thread.currentThread().getName() +"執行緒"); } } ``` Runnable 介面中定義有一個 run 方法,所以實現 Runnable 介面,就必須實現 run 方法。實際上 java.lang.Thread 類也實現了 Runnable 介面。 ![](https://img2020.cnblogs.com/other/1850167/202006/1850167-20200630085543739-117911712.png) 建立執行緒: ```java public class Main { public static void main(String[] args) { // 建立 Thread 例項,並給將要建立的執行緒給命名 Thread thread = new Thread(new DemoRunnable(), "DemoRunnable 子執行緒"); // 建立一個執行緒 thread.start(); System.out.println("main 執行緒"); } } ``` 執行結果 ![](https://img2020.cnblogs.com/other/1850167/202006/1850167-20200630085544947-1642320774.png) 同樣也實現了與繼承 Thread 方式一樣的結果。 建立 Thread 例項時,向新建立的 Thread 例項中傳入了一個實現 Runnable 介面的物件的引數。 ![](https://img2020.cnblogs.com/other/1850167/202006/1850167-20200630085545230-1911157614.png) Thread 中初始化 Thread#init 的具體實現: ```java private void init(ThreadGroup g, Runnable target, String name, long stackSize, AccessControlContext acc, boolean inheritThreadLocals) { if (name == null) { throw new NullPointerException("name cannot be null"); } // 給當前建立的 thread 例項中賦值執行緒名 this.name = name; // 將要建立的執行緒的父執行緒即當前執行緒 Thread parent = currentThread(); // 新增到執行緒組操作 SecurityManager security = System.getSecurityManager(); if (g == null) { if (security != null) { g = security.getThreadGroup(); } if (g == null) { g = parent.getThreadGroup(); } } g.checkAccess(); if (security != null) { if (isCCLOverridden(getClass())) { security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION); } } // 執行緒組中新增為啟動的執行緒數 g.addUnstarted(); this.group = g; // 設定父執行緒的一些屬性到當前將要建立的執行緒 this.daemon = parent.isDaemon(); this.priority = parent.getPriority(); if (security == null || isCCLOverridden(parent.getClass())) this.contextClassLoader = parent.getContextClassLoader(); else this.contextClassLoader = parent.contextClassLoader; this.inheritedAccessControlContext = acc != null ? acc : AccessController.getContext(); // 將當前傳入 target 的引數,賦值給當前 Thread 物件,使其持有 已實現 Runnable 介面的例項 this.target = target; setPriority(priority); if (inheritThreadLocals && parent.inheritableThreadLocals != null) this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); // 設定執行緒的堆疊大小 this.stackSize = stackSize; // 給建立的執行緒一個 id tid = nextThreadID(); } ``` 上面程式碼建立 thread 物件時的 init 方法,通過傳入 Runnable 的例項物件,thread 物件中就持有該物件。 建立 thread 物件後,呼叫 start() 方法,該執行緒就執行持有 Runnable 實現類物件的 run() 方法。 ![](https://img2020.cnblogs.com/other/1850167/202006/1850167-20200630085545502-1839731122.png) 例如本文中案例,就會執行 DemoRunnable#run 方法的邏輯。 這兩種方法建立執行緒的方式,具體使用哪種,根據自身需求選擇。如果需要繼承其他非 Thread 類,就需要使用 Runnable 介面。 # 執行緒狀態 Java 執行緒每個時間點都存在於6種狀態中一種。 | 狀態 | 描述 | | :-----: | ----- | | NEW | 初始狀態,thread 物件呼叫 start() 方法前 | | RUNNABLE | 執行狀態,執行緒 start() 後的就緒或執行中 | | BLOCKED | 阻塞狀態,執行緒獲得鎖後的鎖定狀態 | | WAITING | 等待狀態,執行緒進入等待狀態,不會被分配時間片,需要等待其他執行緒來喚醒 | | TIME_WAITING | 超時等待狀態,同樣不分配時間片,當時間達到設定的等待時間後自動喚醒 | | TERMINATED | 終止狀態,表示當前執行緒執行完成 | 其中 NEW、RUNNABLE、TERMINATED 比較好理解,現在主要針對 BLOCKED、WAITING 和 TIME_WAITING 進行案例講解。 ## BLOCKED **阻塞狀態** 是將兩個執行緒之間處於競爭關係,同時在呼叫 run 時進行加鎖。 首先還是使用上面 Runnable 實現的方式進行改造。 ```java public class DemoRunnable implements Runnable { @Override public void run() { // 通過對DemoRunnable加同步鎖,進行無限迴圈不退出 synchronized (DemoRunnable.class){ while (true){ System.out.println("java.lang.Runnable 建立的 "+ Thread.currentThread().getName() +"執行緒"); } } } } ``` 先競爭到 DemoRunnable 類的執行緒進入 run 會一直執行下去,未競爭到的執行緒則會一直處於阻塞狀態。 建立兩個執行緒 ```java public class Main { public static void main(String[] args) { // 建立兩個執行緒測試 new Thread(new DemoRunnable(), "test-blocked-1") .start(); new Thread(new DemoRunnable(), "test-blocked-2") .start(); } } ``` 通過分析執行後的執行緒如圖: ![](https://img2020.cnblogs.com/other/1850167/202006/1850167-20200630085545910-734242184.png) 可以得知執行緒`test-blocked-1`競爭到 DemoRunnable 類,一直都在執行 while 迴圈,所以狀態為 RUNNABLE。由於 DemoRunnable#run 中加了同步鎖鎖住 DemoRunnable 類,所以`test-blocked-2`一直處於 BLOCKED 阻塞狀態。 ## WAITING **等待狀態** 執行緒是不被分配 CPU 時間片,執行緒如果要重新被喚醒,必須顯示被其它執行緒喚醒,否則會一直等待下去。 實現等待狀態例子 ```java public class DemoRunnable implements Runnable { @Override public void run() { while (true){ // 呼叫 wait 方法,使執行緒在當前例項上處於等待狀態 synchronized (this){ try { this.wait(); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("java.lang.Runnable 建立的 "+ Thread.currentThread().getName() +"執行緒"); } } } } // 建立執行緒 public class Main { public static void main(String[] args) { new Thread(new DemoRunnable(), "test-waiting") .start(); } } ``` 建立該例項執行緒後,分析 test-waiting 執行緒,該執行緒處於 WAITING 狀態。 ![](https://img2020.cnblogs.com/other/1850167/202006/1850167-20200630085546142-1709889745.png) ## TIME_WAITING **超時等待狀態** 執行緒也是不被分配 CPU 時間片,但是它通過設定的間隔時間後,可以自動喚醒當前執行緒。也就是說,將等待狀態的執行緒加個時間限制就是超時等待狀態。 只需對上面 WAITING 狀態案例增加 wait 時間限制。 ```java public class DemoRunnable implements Runnable { @Override public void run() { while (true){ synchronized (this){ try { // 增加等待時長 this.wait(1000000, 999999); } catch (InterruptedException e) { e.printStackTrace(); } System.out.println("java.lang.Runnable 建立的 "+ Thread.currentThread().getName() +"執行緒"); } } } } ``` 分析執行緒結果,可以看到 test-time_waiting 執行緒處於超時等待狀態,使用 sleep 睡眠時,執行緒也是屬於超時等待狀態。 ![](https://img2020.cnblogs.com/other/1850167/202006/1850167-20200630085546452-442956490.png) 執行緒狀態之間的轉換,如圖(來源網路): ![](https://img2020.cnblogs.com/other/1850167/202006/1850167-20200630085546758-937417712.jpg) # Thread 常用方法 ## currentThread() currentThread 是獲取當前執行緒例項,返回 Thread 物件,這是一個靜態方法,使用如下 ```java Thread.currentThread(); ``` ## start() start 方法是啟動執行緒的入口方法,這個就是上面實現建立執行緒例子中的 start 方法。 ## run() run 方法是執行緒建立後,執行緒會主動呼叫 run 方法執行裡面的邏輯。 ## join() join 方法即執行緒同步,比如上繼承 Thread 方法實現建立執行緒的例子中,如果在 thread.start() 後呼叫 thread.join() 方法,則 main 執行緒列印的資訊一定在子執行緒列印的資訊之後。這裡的 main 執行緒會等待子執行緒執行完後,再繼續執行。 ## getName() getName 返回執行緒名稱。 ## getId() 獲取執行緒 Id,這是返回一個 long 型別的 Id 值。 ## setDaemon() setDaemon(boolean on) 方法是設定執行緒型別,setDaemon 接受一個 boolean 型別引數。設定為 true 時,執行緒型別為守護執行緒,設定為 false 時,執行緒型別為使用者執行緒。 ## yield() yield 方法是執行緒讓步,讓當前執行緒進入就緒狀態,去執行其它相同優先順序的執行緒,但不一定會執行其他執行緒,有可能讓步後的執行緒再次被執行。 ## setPriority() setPriority(int newPriority) 是設定執行緒執行的優先順序,數值為1~10,預設值為5,數值越大執行緒越先執行。 ## interrupt() interrupt 方法的作用是中斷執行緒,但是它還是會繼續執行。它只是表示其他執行緒給打了箇中斷標誌。 ## interrupted() interrupted 方法是檢查當前執行緒是否被中斷。呼叫此方法時會清除該執行緒的中斷標誌。 ## isInterrupted() isInterrupted 方法檢測當前執行緒是否被中斷,如果被中斷了,也不會清除中斷標誌。 # 總結 > 本文對執行緒的常用功能及概念進行了分析,主要是講解單執行緒的一些操作,執行緒操作的使用在生產中是極容易出現問題的,所以在掌握概念和使用後,需要多研究,多思考應用的設計及實現。在掌握多執行緒操作時,必須對這些的基本使用和概念進行掌握,今後會出進一步對多執行緒分析的文章。 **推薦閱讀** [《你必須會的 JDK 動態代理和 CGLIB 動態代理》](https://ytao.top/2020/04/05/20-java-proxy/) [《Dubbo 擴充套件點載入機制:從 Java SPI 到 Dubbo SPI》](https://ytao.top/2020/03/22/19-dubbo-spi/) [《volatile 手摸手帶你解析》](https://ytao.top/2020/03/15/18-vol