Java 執行緒基礎,從這篇開始
阿新 • • 發佈:2020-06-30
![](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