## 07 深入Thread類—線程API精講
> 每個人的生命都是一只小船,理想是小船的風帆。
> ——張海迪
前面幾節我們都在圍繞著如何創建 Thread 和 啟動 Thread 做分析。上節我們講解了 Thread 的幾種狀態,以及狀態間的變化。有些狀態的變化是被動發生的,比如 run 方法執行完后進入 TERMINATED 狀態。不過更多時候,狀態的變化是由于主動調用了某些方法。而這些方法大多數是 Thread 類的 API。本小結,我們就來重點學習下 Thread 類暴露出來的 API。
## sleep 方法
顧名思義,線程的 sleep 方法會使線程休眠指定的時間長度。休眠的意思是,當前邏輯執行到此不再繼續執行,而是等待指定的時間。但在這段時間內,該線程持有的 monitor 鎖(鎖在后面會講解,這里可以認為對共享資源的獨占標志)并不會被放棄。我們可以認為線程只是工作到一半休息了一會,但它所占有的資源并不會交還。這樣設計很好理解,因為線程在 sleep 的時候可能是處于同步代碼塊的中間位置,如果此時把鎖放棄,就違背了同步的語義。所以 sleep 時并不會放棄鎖,等過了 sleep 時長后,可以確保后面的邏輯還在同步執行。

sleep 方法有兩個重載,分別是:
~~~java
public static native void sleep(long millis) throws InterruptedException;
public static void sleep(long millis, int nanos) throws InterruptedException
~~~
兩者的區別只是一個支持休眠時間到毫秒級,另外一個到納秒級。但其實第二個并不能真的精確到納秒級別,我們來看第二個重載方法代碼:
~~~java
public static void sleep(long millis, int nanos)
throws InterruptedException {
if (millis < 0) {
throw new IllegalArgumentException("timeout value is negative");
}
if (nanos < 0 || nanos > 999999) {
throw new IllegalArgumentException(
"nanosecond timeout value out of range");
}
if (nanos >= 500000 || (nanos != 0 && millis == 0)) {
millis++;
}
sleep(millis);
}
~~~
可以清楚的看到,最終調用的還是第一個毫秒級別的 sleep 方法。而傳入的納秒會被四舍五入。如果大于 50 萬,毫秒 ++,否則納秒被省略。
## yield 方法
yield 方法我們平時并不常用。yield 單詞的意思是讓路,在多線程中意味著本線程愿意放棄 CPU 資源,也就是可以讓出 CPU 資源。不過這只是給 CPU 一個提示,當 CPU 資源并不緊張時,則會無視 yield 提醒。如果 CPU 沒有無視 yield 提醒,那么當前 CPU 會從 RUNNING 變為 RUNNABLE 狀態,此時其它等待 CPU 的 RUNNABLE 線程,會去競爭 CPU 資源。講到這里有個問題,剛剛 yield 的線程同為 RUNNABLE 狀態,是否也會參與競爭再次獲得 CPU 資源呢?經過我大量測試,剛剛 yield 的線程是不會馬上參與競爭獲得 CPU 資源的。
我們看下面測試代碼:
~~~java
public class YieldExampleClient {
public static void main(String[] args) {
Thread xiaoming = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println("小明--" + i);
// if (i == 2) {
// Thread.yield();
// }
}
});
Thread jianguo = new Thread(() -> {
for (int i = 0; i < 10; i++) {
System.out.println("建國--" + i);
}
});
xiaoming.start();
jianguo.start();
}
}
~~~
可以看到啟動兩個線程打印,控制臺輸出如下:
~~~
小明--0
小明--1
小明--2
小明--3
小明--4
小明--5
小明--6
小明--7
小明--8
小明--9
建國--0
建國--1
建國--2
建國--3
建國--4
建國--5
建國--6
建國--7
建國--8
建國--9
~~~
每次結果有所區別,但是一般都是小明輸出到 5 以后,建國才開始輸出。這一是因為線程啟動需要時間,另外也是因為 CPU 緊張, jianguo 線程在排隊。
我們放開小明線程注解部分,讓輸出到 xiaoming 線程輸出到 2 的時候 yield ,看看會怎么樣。輸出如下:
~~~
小明--0
小明--1
小明--2
建國--0
建國--1
......
~~~
我們看前四行,xiaoming 先獲得了 CPU 的使用權,不過在打印到 2 的時候調用了 yield 方法,提示可以讓出 CPU 的使用權,而此時 CPU 接受了提示,從而讓建國獲得了 CPU 的使用權。我嘗試建立更多的線程,多次嘗試,發現小明打印到 2 的時候,肯定會切換為其它線程打印。不過如果 CPU 資源豐富,那么會無視 yield 方法,xiaoming 也無需讓出 CPU 資源。
yield 方法為了提升線程間的交互,避免某個線程長時間過渡霸占 CPU 資源。但 yield 在實際開發中用的比較少,源碼的注解也提到這一點:“*It is rarely appropriate to use this method.*”。
## currentThread 方法
我們前幾節中已經使用過該方法,這是一個靜態方法,用于獲取當前線程的實例。用法很簡單,如下:
~~~java
Thread.currentThread();
~~~
拿到線程的實例后,我們還可以獲取 Thread 的 名稱:
~~~java
Thread.currentThread().getName();
~~~
這兩個方法在之前例子中我們都使用過,也比較簡單,就不再贅述。
此外我們還可以獲取線程 ID :
~~~java
Thread.currentThread().getId();
~~~
## setPriority 方法
此方法用于設置線程的優先級。每個線程都有自己的優先級數值,當 CPU 資源緊張的時候,優先級高的線程獲得 CPU 資源的概率會更大。請注意僅僅是概率會更大,并不意味著就一定能夠先于優先級低的獲取。這和搖車牌號一個道理,我現在中簽概率是標準的 9 倍,但搖中依然搖搖無期。而身邊卻時不時的出現第一次搖號就中的朋友。如果在 CPU 比較空閑的時候,那么優先級就沒有用了,人人都有肉吃,不需要搖號了。
優先級別高可以在大量的執行中有所體現。在大量數據的樣本中,優先級高的線程會被選中執行的次數更多。
最后我們看下 setPriority 的源碼:
~~~java
public final void setPriority(int newPriority) {
ThreadGroup g;
checkAccess();
if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) {
throw new IllegalArgumentException();
}
if((g = getThreadGroup()) != null) {
if (newPriority > g.getMaxPriority()) {
newPriority = g.getMaxPriority();
}
setPriority0(priority = newPriority);
}
}
~~~
Thread 有自己的最小和最大優先級數值,范圍在 1-10。如果不在此范圍內,則會報錯。另外如果設置的 priority 超過了線程所在組的 priority ,那么只能被設置為組的最高 priority 。最后通過調用 native 方法 setPriority0 進行設置。
## interrupt 相關方法
interrupt 的意思是打斷。調用了 interrupt 方法后,線程會怎么樣?不知道你的答案是什么。我在第一次學習 interrupt 的時候,第一感覺是讓線程中斷。其實,并不是這樣。inerrupt 方法的作用是讓可中斷方法,比如讓 sleep 中斷。也就是說其中斷的并不是線程的邏輯,中斷的是線程的阻塞。這一點在本小結一開始就要徹底搞清池,否則帶著錯誤的想法會影響學習的效果。
那么 interrupt 方法調用后,對未使用可中斷方法的線程有影響嗎?我們做個簡單的實驗,代碼如下:
~~~java
public class InterruptClient {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(()->{
for(int i=0; i<100 ;i++){
System.out.println("I'm doing my work");
System.out.println("I'm interrupted?"+Thread.currentThread().isInterrupted());
}
});
thread.start();
Thread.sleep(1);
thread.interrupt();
}
}
~~~
線程 run 方法中沒有調用可中斷方法,只是輸出**I’m doing my work**,另外還會輸出自己的中斷狀態。而主線程會 sleep 一毫秒,留時間給 thread 線程啟動,然后調用 thread 線程的 interrupt 方法。我截取其中關鍵一段輸出如下:
~~~
I'm doing my work
I'm interrupted?false
I'm doing my work
I'm interrupted?true
I'm doing my work
I'm interrupted?true
~~~
這段后面的輸出一直到結束,都在重復 “I’m doing my work I’m interrupted?true“,這說明兩個問題:
1. 調用 interrupt 方法,并不會影響可中斷方法之外的邏輯。線程不會中斷,會繼續執行。這里的中斷概念并不是指中斷線程;
2. 一旦調用了 interrupt 方法,那么線程的 interrupted 狀態會一直為 ture(沒有通過調用可中斷方法或者其他方式主動清除標識的情況下);
通過上面實現我們了解了 interrupt 方法中斷的不是線程。它中斷的其實是可中斷方法,如 sleep 。可中斷方法被中斷后,會把 interrupted 狀態歸位,改回 false 。
我們還是做個實驗,代碼如下:
~~~java
public class InterruptSleepClient {
public static void main(String[] args) throws InterruptedException {
Thread xiaopang = new Thread(()->{
for(int i=0; i<100 ;i++){
System.out.println("I'm doing my work");
try {
System.out.println("I will sleep");
Thread.sleep(1000);
} catch (InterruptedException e) {
System.out.println("My sleeping was interrupted");
}
System.out.println("I'm interrupted?"+Thread.currentThread().isInterrupted());
}
});
xiaopang.start();
Thread.sleep(1);
xiaopang.interrupt();
}
}
~~~
這次干活的小胖比較懶,每次干完活都要休息一秒鐘。有一次被讓他干活的老師發現,把他叫醒了。但是后來看他照睡不誤,也就隨他去了。這段代碼執行結果如下:
~~~
I'm doing my work
I will sleep
My sleeping was interrupted
I'm interrupted? false
I'm doing my work
I will sleep
I'm interrupted? false
I'm doing my work
I will sleep
I'm interrupted? false
~~~
可以看到當 xiaopang.interrupt () 執行后,睡眠中的 xiaopang 被喚醒了。這里額外需要注意的是,此時 xiaopang 線程的 interrupted 狀態還是 false 。因為可中斷線程會捕獲中斷的信號,并且會清除掉 interrupted 標識。因此輸出的 “I’m interrupted ?” 全部是 false 。
最后我們再看一下靜態方法 interrupted 。這個方法其實和成員方法 isInterrupted 方法類似,都是返回了 interrupted 狀態。不同就是 interrupted 方法返回狀態后,如果為 true 則會清除掉狀態。而 isInterrupted 則不會。上面第一段測試代碼已經驗證了這一點,被打斷后,調用 isInterrupted 一直返回 true。
下面我們來驗證下 interrupted 是否會清除標識位。把第一段代碼稍微改一下:
~~~java
public class InterruptedClient {
public static void main(String[] args) throws InterruptedException {
Thread thread = new Thread(()->{
for(int i=0; i<100 ;i++){
System.out.println("I'm doing my work");
//原代碼 System.out.println("I'm interrupted?"+Thread.currentThread().isInterrupted());
System.out.println("I'm interrupted?"+Thread.interrupted());
}
});
thread.start();
Thread.sleep(1);
thread.interrupt();
}
}
~~~
改動已經在注解中說明,僅僅是改了獲取 interrupted 狀態的方法。但輸出結果卻是不一樣的:
~~~
I'm doing my work
I'm interrupted?false
I'm doing my work
I'm interrupted?false
I'm doing my work
I'm interrupted?true
I'm doing my work
I'm interrupted?false
I'm doing my work
I'm interrupted?false
~~~
可以看到在輸出 “I’m interrupted?true” 后,中斷狀態又變回了 false。
通過以上講解,可以看出 interrupt 方法只是設置了中斷標識位,這個標識位只對可中斷方法會產生作用。不過我們還可以利用它做更多的事情,比如說如果線程的 run 方法中這么寫:
~~~java
while(!isInterrupted()){
//do somenting
}
~~~
這樣主線程中可以通過調用此線程的 interrupt 方法,讓其推出運行。此時 interrupted 的含義就真的是線程退出了。不過假如你的 while 循環中調用了可中斷方法,那么就會有干擾。
## join 方法
最后我們再講解一個重要的方法 join。這個方法功能強大,也很實用。我們用它能夠實現并行化處理。比如主線程需要做兩件沒有相互依賴的事情,那么可以起 A、B 兩個線程分別去做。通過調用 A、B 的 join 方法,讓主線程 block 住,直到 A、B 線程的工作全部完成,才繼續走下去。我們來看下面這段代碼:
~~~java
public class JoinClient {
public static void main(String[] args) throws InterruptedException {
Thread backendDev = createWorker("backed dev", "backend coding");
Thread frontendDev = createWorker("frontend dev", "frontend coding");
Thread tester = createWorker("tester", "testing");
backendDev.start();
frontendDev.start();
// backendDev.join();
// frontendDev.join();
tester.start();
}
public static Thread createWorker(String role, String work) {
return new Thread(() -> {
System.out.println("I finished " + work + " as a " + role);
});
}
}
~~~
這段代碼中,我們把 join 方法去掉。執行結果如下:
~~~
I finished backend coding as a backed dev
I finished testing as a tester
I finished backend coding as a frontend dev
~~~
我們期望的是前端和后端開發完成工作后,測試才開始測試。但從輸出結果看并非如此。要想實現這個需求,我們只需把注釋打開,讓**backendDev**和**frontendDev**先做 join 操作,此時主線程會被 block 住。直到**backendDev**和**frontendDev**線程都執行結束,才會繼續往下執行。輸出如下:
~~~
I finished backend coding as a backed dev
I finished frontend coding as a frontend dev
I finished testing as a tester
~~~
可以看到現在的輸出完全符合我們的期望。可見調用 join 方法后 block 的并不是被調用的**backendDev**或**frontendDev 線程**,而是調用方線程,這個需要牢記。
## 總結
本小結講解了 Thread 的幾個常用的方法,這些方法在我們實際開發中會經常用到的,需要我們認真學習和理解。有些已經被棄用的方法沒有再講解,比如 stop 方法。關于更多的方法,其實讀者可以直接閱讀 Thread 源代碼,Thread 類的注解寫得相當詳細。其實很多時候我們自己動手直接閱讀源代碼和注解,是更為快捷的學習方式,而且也更為權威。
下一節我們繼續講解線程的相關操作 wait ()、notify ()、notifyAll ()。這些方法也會改變線程的狀態,但并不是 Thread 的 API 。
- 前言
- 第1章 Java并發簡介
- 01 開篇詞:多線程為什么是你必需要掌握的知識
- 02 絕對不僅僅是為了面試—我們為什么需要學習多線程
- 03 多線程開發如此簡單—Java中如何編寫多線程程序
- 04 人多力量未必大—并發可能會遇到的問題
- 第2章 Java中如何編寫多線程
- 05 看若兄弟,實如父子—Thread和Runnable詳解
- 06 線程什么時候開始真正執行?—線程的狀態詳解
- 07 深入Thread類—線程API精講
- 08 集體協作,什么最重要?溝通!—線程的等待和通知
- 09 使用多線程實現分工、解耦、緩沖—生產者、消費者實戰
- 第3章 并發的問題和原因詳解
- 10 有福同享,有難同當—原子性
- 11 眼見不實—可見性
- 12 什么?還有這種操作!—有序性
- 13 問題的根源—Java內存模型簡介
- 14 僵持不下—死鎖詳解
- 第4章 如何解決并發問題
- 15 原子性輕量級實現—深入理解Atomic與CAS
- 16 讓你眼見為實—volatile詳解
- 17 資源有限,請排隊等候—Synchronized使用、原理及缺陷
- 18 線程作用域內共享變量—深入解析ThreadLocal
- 第5章 線程池
- 19 自己動手豐衣足食—簡單線程池實現
- 20 其實不用造輪子—Executor框架詳解
- 第6章 主要并發工具類
- 21 更高級的鎖—深入解析Lock
- 22 到底哪把鎖更適合你?—synchronized與ReentrantLock對比
- 23 按需上鎖—ReadWriteLock詳解
- 24 經典并發容器,多線程面試必備—深入解析ConcurrentHashMap上
- 25 經典并發容器,多線程面試必備—深入解析ConcurrentHashMap下
- 26不讓我進門,我就在門口一直等!—BlockingQueue和ArrayBlockingQueue
- 27 倒數計時開始,三、二、一—CountDownLatch詳解
- 28 人齊了,一起行動—CyclicBarrier詳解
- 29 一手交錢,一手交貨—Exchanger詳解
- 30 限量供應,不好意思您來晚了—Semaphore詳解
- 第7章 高級并發工具類及并發設計模式
- 31 憑票取餐—Future模式詳解
- 32 請按到場順序發言—Completion Service詳解
- 33 分階段執行你的任務-學習使用Phaser運行多階段任務
- 34 誰都不能偷懶-通過 CompletableFuture 組裝你的異步計算單元
- 35拆分你的任務—學習使用Fork/Join框架
- 36 為多線程們安排一位經理—Master/Slave模式詳解
- 第8章 總結
- 37 結束語