第 2 章和第 3 章主要是從概念上區了解 Thread,本章中,我們將細致的學習 Thread 所有 API 的作用以及用法,
4.1、執行緒 sleep
sleep 是一個靜態方法,其有兩個多載方法,其中一個需要傳入毫秒數,另外一個既需 要毫秒數也需要納秒數,
4.1.1、sleep 方法介紹
public static void sleep (long millis) throws InterruptedException
public static void sleep (long millis, int nanos) throws InterruptedException
sleep 方法會使當前執行緒進入指定毫秒數的休眠,暫停執行,雖然給定了一個休眠的時間, 但是最終要以系統的定時器和調度器的精度為準,休眠有一個非常重要的特性,那就是其不 會放棄 monitor 鎖的所有權(執行緒同步和鎖的時候會重點介紹 monitor),下面我們來看 一個簡單的例子,
package com.bjsxt.chapter04.demo01;
public class ThreadSleep {
public static void main(String[] args) {
// 子執行緒
new Thread(()->{
long startTime = System.currentTimeMillis();
sleep(2000);// Thread.sleep是使得當前執行緒休眠的時間,不是所有執行緒的休眠時間,
long endTime = System.currentTimeMillis();
System.out.printf("Total spent %d ms.\n",endTime - startTime);
}).start();
// main 執行緒
long startTime = System.currentTimeMillis();
sleep(3000);
long endTime = System.currentTimeMillis();
System.out.printf("main thread spent %d ms.",endTime - startTime);
}
private static void sleep(long ms) {
try{
Thread.sleep(ms);
}catch (InterruptedException e) {
e.printStackTrace();
}
}
}
運行結果

在上面的例子中,我們分別在自定義的執行緒和主執行緒中進行了休眠,每個執行緒的休眠互 不影響,從結果看,Thread.sleep 只會導致當前執行緒進入指定時間的休眠,
4.1.2、使用 TimeUniT 替代 Thread.sleep
在 JDK1.5 以后,JDK 引入了一個列舉 TimeUnit,其對 sleep 方法提供了很好的封裝, 使用它可以省去時間單位的換算步驟,比如執行緒想休眠 3 小時 24 分 17 秒 88 毫秒,使用 TimeUnit 來實作就非常簡便優雅了:
package com.bjsxt.chapter04.demo01;
import java.util.concurrent.TimeUnit;
public class ThreadTimeUnit {
public static void main(String[] args) {
try{
long startTime = System.currentTimeMillis();
//Thread.sleep(12257088L);
Thread.sleep(17088);
long endTime = System.currentTimeMillis();
System.out.printf("main thread slept spent %d ms \n",endTime - startTime);
startTime = System.currentTimeMillis();
//TimeUnit.HOURS.sleep(3);
//TimeUnit.MINUTES.sleep(24);
TimeUnit.SECONDS.sleep(17);
TimeUnit.MILLISECONDS.sleep(88);
endTime = System.currentTimeMillis();
System.out.printf("main thread slept spent %d ms \n",endTime - startTime);
}catch (InterruptedException e) {
e.printStackTrace();
}
}
}
運行結果

同樣的時間表達,TimeUnit 顯然清晰很多,強烈建議在使用 Thread.sleep 的地方, 完全使用 TimeUnit 來代替,因為 sleep 能做的事情,TimeUnit 全部都能完成,并且可以 做的更好,后面內容中,我將全部采用 TimeUnit 替代 sleep,
4.1.3、Thread.sleep(0)
Thread.sleep(0) 表示掛起 0 毫秒,你可能覺得沒作用,其實 Thread.sleep(0) 并 非是真的要執行緒掛起 0 毫秒,意義在于這次呼叫 Thread.sleep(0)的當前執行緒確實的被凍 結了一下,讓其他執行緒有機會優先執行,Thread.sleep(0) 是你的執行緒暫時放棄 cpu,也 就是釋放一些未用的時間片給其他執行緒或行程使用,就相當于一個讓位動作,
在執行緒中,呼叫 sleep(0)可以釋放 cpu 時間,讓執行緒馬上重新回到就緒佇列而非等 待佇列,sleep(0)釋放當前執行緒所剩余的時間片(如果有剩余的話),這樣可以讓操作系 統切換其他執行緒來執行,提升效率,
4.2、執行緒 yield
4.2.1、yield 方法介紹
yield 方法屬于一種啟發式的方法,其會提醒調度器我愿意放棄當前的 CPU 資源,如 果 CPU 的資源不緊張,則會忽略這種提醒,
呼叫 yield 方法會使當前執行緒從 Running 狀態切換到 Runnable 狀態,一般這個方法 不太常用,接下來我們看一個案例:
package com.bjsxt.chapter04.demo02;
import java.util.stream.IntStream;
import java.util.stream.Stream;
public class ThreadYield {
public static void main(String[] args) {
// IntStream.range(0, 2).mapToObj(ThreadYield::create).forEach(Thread::start);
Thread t1 = create(0);
Thread t2 = create(1);
t1.start();
t2.start();
}
private static Thread create(int index){
return new Thread(()->{
if(index == 0)
Thread.yield();
System.out.println(index);
});
}
}
運行結果


上面的程式運行多次,你會發現輸出結果不一致,有時候是 0 最先列印出來,有時候是 1 最先列印出來,根據代碼運行結果分析如下:
第一個執行緒如果最先獲得了 CPU 資源,它會比較謙虛,主動告訴 CPU 調度器試放原本 屬于自己的資源,但是 yield 只是一個提示(hint),CPU 調度器并不會擔保每次都能滿 足 yield 提示,
4.2.2、yield 和 sleep
看過前面的內容之后,會發現 yield 和 sleep 有一些混淆的地方,在 JDK1.5 以前的
版本中 yield 的方法事實上是呼叫了 sleep(0),但是他們之間存在著本質的區別,具體如 下:?
sleep 會導致當前執行緒暫停指定的時間,沒有 CPU 時間片的消耗; ?
yield 只是對 CPU 調度器的一個提示,如果 CPU 調度器沒有忽略這個提示,它會導致 執行緒背景關系的切換; ?
sleep 會使執行緒短暫 block,會在給定的時間內試放 CPU 資源; ?
yield 會使 Running 狀態的 Thread 進入 Runnable 狀態(如果 CPU 調度器沒有忽略 這個提示的話); ?
sleep 幾乎百分之百的完成了給定時間的休眠,而 yield 的提示并不能一定保證,
4.3、設定執行緒優先級
public final void setPriority (int newPriority)
public final int getPriority()
4.3.1、執行緒優先級介紹
行程有行程的優先級,執行緒同樣也有優先級,理論上是優先級比較高的執行緒會獲取優先 被 CPU 調度的機會,但是事實上往往并不會如你所愿,設定執行緒的優先級同樣也是一個 hint 操作,具體如下, ?
對于 root 用戶,他會 hint 作業系統你想要設定的優先級別,否則它會被忽略, ?
如果 CPU 比較忙,設定優先級可能會獲得更多的 CPU 時間片,但是閑時優先級的高低 幾乎不會有任何作用,
所以,不要在程式設計當中企圖使用執行緒優先級系結某些特定的業務,或者讓業務嚴重 依賴于執行緒優先級,這可能會讓你大失所望,
舉個簡單的例子,可能不同情況下的與運行效果不會完全一樣,但是我們只是想讓優先 級比較高的執行緒獲得更多的資訊輸出機會,代碼如下:
package com.bjsxt.chapter04.demo03;
public class ThreadPriority01 {
public static void main(String[] args) {
Thread t1 = new Thread(()->{
int i = 0;
while(true){
System.out.printf("t1 run times : %d\n",i++);
}
});
t1.setPriority(3);
Thread t2 = new Thread(()->{
int i = 0;
while (true){
System.out.printf("t2 run times : %d\n",i++);
}
});
t2.setPriority(10);
t1.start();t2.start();
}
}
運行結果


運行上面的程式,會發現 t2 出現的頻率很明顯要高一些,當然這也和筆者當前 CPU 的 資源情況有關系,不同情況下的運行會有不一樣的結果,
4.3.2、執行緒優先級原始碼分析
設定執行緒的優先級,只需要呼叫 setPriority 方法即可,下面我們打開 Thread 原始碼, 一起來分析一下:
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);
}
}
通過上面原始碼的分析,我們可以看出,執行緒的優先級不能小于 1 也不能大于 10,如果 指定的執行緒優先級大于執行緒所在 group 的優先級,那么指定的優先級將會失敗,取而代之 的是 group 的最大優先級,下面我們通過一個例子來證明一下:
package com.bjsxt.chapter04.demo03;
public class ThreadPriority02 {
public static void main(String[] args) {
ThreadGroup group = new ThreadGroup("test");
group.setMaxPriority(7);
Thread t1 = new Thread(group,()->{
});
t1.setPriority(10);
System.out.println(t1.getPriority());
}
}
運行結果

上面的結果輸出為 7,而不是 10,因為它超過了所在執行緒組的優先級,
4.3.3、關于優先級的一些總結
一般情況下,不會對執行緒設定優先級別,更不會讓某些業務嚴重的依賴執行緒的優先級別, 比如權重,借助優先級設定某個任務的權重,這種方式是不可取的,一般定義執行緒的時候使 用默認的優先級就好了,那么執行緒默認的優先級是多少呢?
執行緒默認的優先級和它的父類保持一致,一般情況下都是 5,因為 main 執行緒的優先級 就是 5,所以它派生出來的執行緒都是 5,代碼如下:
package com.bjsxt.chapter04.demo03;
public class ThreadPriority03 {
public static void main(String[] args) {
// 元資料區域 堆 執行緒共享
// main 執行緒有 程式計數器 java虛擬機堆疊 本地方法堆疊
// main 執行緒的java虛擬機堆疊中存放一個執行緒物件 t1 的參考
// t1 存放在 堆
Thread t1 = new Thread("t1");
// main 執行緒 通過地址獲取 t1 的優先級,輸出
System.out.println("t1 thread priority is :"+t1.getPriority());
// main 執行緒的java虛擬機堆疊保存一個執行緒物件t2的參考
// t2存放在 堆
Thread t2 = new Thread(()->{
// t2 執行緒有 程式計數器 java虛擬機堆疊 本地方法堆疊
// t2 執行緒創建一個執行緒保存到堆里,t3參考變數保存執行緒的地址
Thread t3 = new Thread();
// t2 執行緒通過t3參考獲取堆里物件的優先級,輸出
System.out.println("t3 thread priority is : "+t3.getPriority());
});
// 通過地址設定t2的優先級為8
t2.setPriority(8);
// main 執行緒通過t2獲取堆中物件執行緒的優先級
System.out.println("t2 thread priority is : "+t2.getPriority());
// main 執行緒通過t2訪問堆中執行緒物件的start方法,呼叫vm本地方法堆疊中的start0方法
// t2執行緒物件 獲取程式計數器 java虛擬機堆疊 本地方法堆疊
// cpu 執行
t2.start();
}
}
運行結果

上面的程式的輸出結果是 t1 的優先級為 5,因為 main 執行緒的優先級是 5;t2 的優先 級是8,因為顯示的將其指定為 8;t3 的優先級為 8,沒有顯示值當,因此其父執行緒保持一 致
4.4、獲取執行緒 ID
public long getId() 獲取執行緒的唯一 ID,執行緒的 ID 在整個 JVM 行程中都會是 唯一的,并且是從 0 開始逐次遞增,如果你在 main 執行緒(main 函式)中創建了一個唯一 的執行緒,并且呼叫 getId()后發現其并不等于 0,也許你會納悶,不應該是從 0 開始的嗎? 之前已經說過了在一個 JVM 行程啟動的時候,實際上是開辟了很多個執行緒,自增序列已經 有了一定的消耗,因此我們自己創建的執行緒絕非第 0 號執行緒,
package com.bjsxt.chapter04.demo04;
import java.util.concurrent.TimeUnit;
public class ThreadId {
public static void main(String[] args){
Thread t1 = new Thread(()->{
try{
TimeUnit.MINUTES.sleep(1);
}catch (InterruptedException e){
e.printStackTrace();
}
});
t1.start();
System.out.println(t1.getId());
while(true){}
}
}
運行結果

jconsole查看其他執行緒,或者也可在代碼中獲取執行緒看一下,其他有那些已經創建了的執行緒~~
package com.bjsxt.chapter04.demo04;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.stream.Collectors;
public class ThreadId {
public static void main(String[] args){
Thread t1 = new Thread(()->{
try{
TimeUnit.MINUTES.sleep(1);
}catch (InterruptedException e){
e.printStackTrace();
}
});
t1.start();
System.out.println(t1.getId());
System.out.println(Thread.currentThread().getId());
Set<Map.Entry<Thread, StackTraceElement[]>> entries = Thread.getAllStackTraces().entrySet();
List<ThreadInfo> infos= new ArrayList<>();
for(Map.Entry<Thread, StackTraceElement[]> t:entries)
{
infos.add(new ThreadInfo(t.getKey().getName(),t.getKey().getId()));
}
List<ThreadInfo> list = infos.stream().sorted(Comparator.comparing(ThreadInfo::getId)).collect(Collectors.toList());
list.forEach(System.out::println);
while(true){}
}
}
class ThreadInfo{
String name;
long id;
public long getId() {
return id;
}
ThreadInfo(String name, long id){
this.id=id;
this.name=name;
}
@Override
public String toString() {
return this.name+"-"+this.id;
}
}
運行結果

看見在自己創建的執行緒里id是14,main執行緒是1,垃圾回收執行緒是3.
4.5、獲取當前執行緒
public static Thread currentThread() 用于回傳當前執行執行緒的參考,這個 方法雖然很簡單,但是使用非常廣泛,我們在后面的內容中會大量的使用該方法,來看一段 示例代碼
package com.bjsxt.chapter04.demo05;
import java.util.concurrent.TimeUnit;
public class ThreadCurrentThread {
public static void main(String[] args) throws InterruptedException{
Thread t1 = new Thread(){
@Override
public void run() {
System.out.println(Thread.currentThread() == this);
}
};
t1.start();
// 保證啟動
TimeUnit.SECONDS.sleep(1);
System.out.println(Thread.currentThread().getName().equals("main"));
}
}
運行結果

4.6、執行緒 interrupt
執行緒 interrupt,是一個非常重要的 API,也是經常使用的方法,與執行緒中斷相關, 相關的 API 有以下幾個,在本節中我們也將 Thread 深入原始碼對其進行詳細的剖析, ?
public void interrupt() ?
public static boolean interrupted() ?
public boolean isInterrupted()
4.6.1、interrupt
以下方法的呼叫會使得當前執行緒進入阻塞狀態,而呼叫當前執行緒的 interrupt 方法, 就可以打斷阻塞, ?
Object 的 wait 方法; ?
Object 的 wait(long)方法;
Object 的 wait(lOng, int)方法; ?
Object 的 sleep(long)方法; ?
Thread 的 sleep(long)方法; ?
Thread 的 join 方法; ?
Thread 的 join(long)方法; ?
Thread 的 join(long, int)方法; ?
InterruptIbleChannel 的 io 操作; ?
Selector 的 wakeup 方法
上述若干方法都會使得當前執行緒進入阻塞狀態,若另外的一個執行緒呼叫被阻塞執行緒的 interrupt 方法,則會打斷這種阻塞,因此這種方法有時會被稱為可中斷方法,記住,打 斷一個執行緒并不等于該執行緒的生命周期結束,僅僅是打斷了當前執行緒的阻塞狀態,
一旦執行緒在阻塞的情況下被打斷,都會拋出一個稱為 InterruptedException 的例外, 這個例外就像一個 signal(信號)一樣通知當前執行緒被打斷了,下面我們來看一個例子:
package com.bjsxt.chapter04.demo06;
import java.util.concurrent.TimeUnit;
public class ThreadInterrupt {
public static void main(String[] args) throws InterruptedException{
Thread t1 = new Thread(()->{
try{
// 先執行
TimeUnit.MINUTES.sleep(1);
}catch (InterruptedException e){
System.out.println("阻塞狀態被中斷");
e.printStackTrace();
}
});
t1.start();
// 保證t1執行緒運行
TimeUnit.MILLISECONDS.sleep(100);
// 后執行
t1.interrupt();
}
}
運行結果

上面的代碼創建了一個執行緒,并且企圖休眠 1 分鐘的時長,不過很可惜,大約在 100 毫秒之后就被主線呼叫 interrupt 方法打斷,程式的執行結果就是”阻塞狀態被中斷“,
interrupt 這個方 法到底做了什 么樣的事情呢 ?在一個線 程內部存在著 名為 interrupt flag 的標識,如果一個執行緒被 interrupt,那么它的 flag 將被設定,但是 如果當前執行緒正在執行可中斷方法被阻塞時,呼叫 interrupt 方法將其中斷,反而會導致 flag 被清除,關于這點我們在后面還會做詳細的介紹,另外有一點需要注意的是,如果一 個執行緒已經是死亡狀態,那么嘗試對其的 interrupt 會直接被忽略,
4.6.2、isInterrupted
isInterrupted 是 Thread 的一個成員方法,它主要判斷當前執行緒是否被中斷,該方 法僅僅是對 interrupt 標識的一個判斷,并不會影響標識發生任何改變,這個與我們即將 學習到的 interrupted 是存在差別的,下面我們看一個簡單的程式:
package com.bjsxt.chapter04.demo06;
import java.util.concurrent.TimeUnit;
public class ThreadInterrupt02 {
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread(()->{
while(true){}
});
t1.setDaemon(true);
t1.start();
TimeUnit.MILLISECONDS.sleep(100);
System.out.println(t1.isInterrupted());// false
t1.interrupt();
System.out.println(t1.isInterrupted());// true
System.out.println(t1.isInterrupted());// true
System.out.println(t1.isInterrupted());// true
System.out.println(t1.isInterrupted());// true
System.out.println(t1.isInterrupted());// true
}
}
上面的代碼中定義了一個執行緒,并且在執行緒的執行單元中(run 方法)寫了一個空的死 回圈,為什么不寫 sleep 呢?因為 sleep 是可中斷方法,會捕獲到中斷信號,從而干擾我 們程式的結果,下面是程式運行的結果,記得手動結束上面的程式運行,或者你也可以將上 面定義的執行緒指定為守護執行緒,這樣就會隨著主執行緒的結束導致 JVM 中沒有非守護執行緒而 自動退出,運行結果如下:

可中斷方法捕獲到了中斷信號(signal)之后,也就是捕獲了 InterruptedException 例外之后會擦除掉 interrupt 的標識,對上面的程式稍作修改,你會發現程式的結果又會 出現很大的不同,示例代碼如下:
package com.bjsxt.chapter04.demo06;
import java.util.concurrent.TimeUnit;
public class ThreadInterrupt03 {
// ??
public static void main(String[] args) throws InterruptedException {
Thread t1 = new Thread() {
@Override
public void run() {
while (true) {
try {
TimeUnit.MINUTES.sleep(1);
} catch (InterruptedException e) {
System.out.printf("I am be interrupted? %s\n", this.isInterrupted());// false
e.printStackTrace();
}
}
}
};
t1.setDaemon(true);
t1.start();
// 短暫的阻塞是為了保證 t1 執行緒已啟動
TimeUnit.MILLISECONDS.sleep(100);
System.out.printf("Thread is interrupted? %s\n", t1.isInterrupted());// false
// 中斷 t1 執行緒的阻塞狀態
t1.interrupt();
TimeUnit.MILLISECONDS.sleep(100);
/*If this thread is blocked in an invocation of the wait(), wait(long), or wait(long, int)
methods of the Objecta class, or of the join(), join(long), join(long, int), sleep(long),
or sleep(long, int), methods of this class,
then its interrupt status will be cleared and it will receive an InterruptedException.*/
System.out.printf("Thread is interrupted? %s\n", t1.isInterrupted());// false
}
}
其實這也不難理解,可中斷方法捕獲到了中斷信號之后,為了不影響執行緒中其他方法的 執行,將執行緒的 interrupt 標識復位是一種很合理的設計,如果執行緒處于阻塞狀態,去打終端標記,會報例外,清空終端標記,所以將會看見false,
運行結果

4.6.3、interrupted
interrupted 是一個靜態方法,雖然其也用于判斷當前執行緒是否被中斷,但是它和成 員方法 isInterrupted 還是又很大的區別,呼叫該方法會直接擦除掉執行緒的 interrupt 標識,需要注意的是,如果當前執行緒被打斷了,那么第一次呼叫 interrupted 方法會回傳 true,并且立即擦除了 interrupt 標識;第二次包括以后的呼叫永遠都會回傳 false,除 非在此期間執行緒又一次被打算,下面設計了一個簡單的例子,來驗證我們的說法:
package com.bjsxt.chapter04.demo06;
import java.util.concurrent.TimeUnit;
public class ThreadInterrupt04 {
public static void main(String[] args) throws InterruptedException{
Thread t1 = new Thread(){
@Override
public void run() {
while(true){
System.out.println(Thread.interrupted());
}
}
};
t1.setDaemon(true);
t1.start();
TimeUnit.MILLISECONDS.sleep(100);
t1.interrupt();
}
}
運行結果

在很多的 false 包圍中發現了一個 true,也就是說 interrupted 方法判斷到了其被 中斷,立即擦除了中斷標識,并且只有這一次回傳 true,后面的都將會是 false,
4.6.4、interrupt 注意事項
打開 Thread 的原始碼,不難發現, interrupted 方法和 interrupted 方法都呼叫了 同一個本地方法:
而 interrupted 靜態方法中該引數則為 true,表示想要擦除:
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
isInterrupted 方法的原始碼中該引數為 false,表示不想擦除:
public boolean isInterrupted() {
return isInterrupted(false);
}
其中引數 ClearInterrupted 主要用來控制是否擦除執行緒 interrupt 的標識,
private native boolean isInterrupted(boolean ClearInterrupted);
在比較詳細的學習了 interrupt 方法之后,大家思考一個問題,如果一個執行緒在沒有 執行可中斷方法之前就被打斷,那么其接下來將執行可中斷方法,比如 sleep 會發生什么 樣的情況呢?下面我們通過一個簡單的實驗來回答這個疑問:
package com.bjsxt.chapter04.demo06;
import java.util.concurrent.TimeUnit;
public class ThreadInterrupt05 {
public static void main(String[] args) throws InterruptedException{
System.out.println(Thread.interrupted());// false
Thread.currentThread().interrupt();// 打標機
System.out.println(Thread.currentThread().isInterrupted());// true
try{
TimeUnit.MINUTES.sleep(1);
}catch (InterruptedException e){
System.out.println("I will be interrupted.\n"+"interrupt flag is "+Thread.currentThread().isInterrupted());
e.printStackTrace();
}
System.out.println(Thread.currentThread().isInterrupted());// ? false
}
}
// 執行緒處于blocked 使用 interrupt標記執行緒狀態,獲取到的是false
// 執行緒使用interrupt 標記,再呼叫sleep,join,wait使他blocked,獲取到的是false
// status will be cleard 回到false
// 如果是false 那么呼叫靜態方法也不會改變值
// 如果是true 那么呼叫靜態方法會改變值
運行結果

通過運行上面的程式,你會發現,如果一個執行緒設定了 interrupt 標識,那么接下來 的可中斷方法會立即中斷,因此最后一步的捕獲中斷信號部分代碼會被執行,(先中斷標記 再阻塞 終端標記會被擦除 會看見false)
4.7、執行緒 join
Thread 的 join 方法同樣是一個非常重要的方法,使用它的特性可以實作很多比較強 大的功能,Thread 的 API 為我們提供了三個不同的 join 方法,具體如下, ?
public final void join() throws InterruptedException ?
public final void join (long millis) throws InterruptedException ?
public final void join (long millis, int nanos) throws InterruptedException
在本節中,將會詳細介紹 join 方法以及如何在實際應用中使用 join方法,
4.7.1、執行緒 join 方法詳解
join 某個執行緒 A,會使當前執行緒 B 進入等待,直到執行緒 A 結束生命周期,或者到達給 定的時間,那么在此期間 B 執行緒是處于 Blocked 的,而不是 A 執行緒,下面就來通過一個簡 單的實體解釋一下 join 方法的基本用法:
package com.bjsxt.chapter04.demo07;
public class ThreadJoin {
public static void main(String[] args) throws InterruptedException{
Thread t1 = new Thread(()->{
for(int i = 0;i<10;i++){
System.out.println("t1#"+i);
}
});
Thread t2 = new Thread(()->{
for(int i = 0;i<10;i++){
System.out.println("t2#"+i);
}
});
// 先啟動執行緒
t1.start();
t2.start();
// 在呼叫join
t1.join();
t2.join();
// 兩個執行緒執行完畢后 會執行main執行緒
for(int i = 0;i<10;i++){
System.out.println("main#"+i);
}
Thread.currentThread().join();// 呼叫自己會一直阻塞
}
}
上面的代碼創建了兩個執行緒,分別啟動,并且呼叫了每個執行緒的 join 方法(注意:join 方法是被主執行緒呼叫的,因此在第一個執行緒還沒結束生命周期的時 候,第二個執行緒的 join 不會得到執行,但是此時,第二個執行緒也已經啟動了),運行上面 的程式,你會發現執行緒一和執行緒二會交替的輸出直到他們結束生命周期,main 執行緒的回圈 才會開始運行,程式輸出如下:


如果你將第三步下面的 join 全部注釋掉,那么三個執行緒將會交替的輸出,程式輸出如 下:
結合java8語法
package com.bjsxt.chapter04.demo07;
public class ThreadJoin02 {
public static void main(String[] args) throws InterruptedException{
Thread t1 = new Thread(()-> printNum());
Thread t2 = new Thread(()-> printNum());
// 先啟動執行緒
t1.start();
t2.start();
// 在呼叫join
t1.join();
t2.join();
// 兩個執行緒執行完畢后 會執行main執行緒
printNum();
Thread.currentThread().join();// 呼叫自己會一直阻塞
}
private static void printNum(){
for(int i = 0;i<10;i++){
System.out.println(Thread.currentThread().getName()+" # "+i);
}
}
}
運行結果


join 方法會使當前執行緒永遠的等待下去,直到期間被另外的執行緒中斷,或者 join 的 執行緒執行結束,當然你也可以使用 join 的另外兩個多載方法,指定毫秒數,在指定的時間 到達之后,當前執行緒也會退出阻塞, 同樣思考一個問題,如果一個執行緒已經結束了生命周期,那么呼叫它的 join 方法的當 前執行緒會被阻塞嗎?
會阻塞,上面的代碼一直在等待中,紅色按鈕一直有,
4.7.2、join 方法結合實戰
本節我們將結合一個實際的案例,來看一下 join 方法的應用場景,假設你有一個 APP, 主要用于查詢航班資訊,你的 APP 是沒有這些實時資料的,當用戶發起查詢請求時,你需 要到各大航空公司的介面獲取資訊,最后統一整理加工回傳到 APP 客戶端,如圖所示,當 然 JDK 自帶了很多高級工具,比如 CountDownLatch 和 CyclicBarrier 等都可以完成類 似的功能,但是僅就我們目前所學的知識,使用 join 方法即可完成下面的功能,

該例子是典型的串行任務區域并行化處理,用戶在 APP 客戶端輸入出發地“北京”和目的地“上海”,服務器接收到這個請求之后,先來驗證用戶的資訊,然后到各大航空公司的 介面查詢資訊,最后經過整理加工回傳給客戶端,每一個航空公司的介面不會都一樣,獲取 的資料格式也不一樣,査詢的速度也存在著差異,如果再跟航空公司進行串行化互動(逐個 地查詢),很明顯客戶端需要等待很長的時間,這樣的話,用戶體驗就會非常差,如果我們 將每一個航空公司的査詢都交給一個執行緒去作業,然后在它們結東作業之后統一對資料進行 整理,這樣就可以極大地節約時間,從而提高用戶體驗效果,
package com.bjsxt.chapter04.demo08;
import java.util.List;
/**
* 航班查詢
*/
public interface FlightQuery {
// run 方法回傳值是 void,定義一個介面,獲取堆里的資料
List<String> get();// 航空公司名稱-查詢時長
}
// 寫一個 main
// new 任意個航空公司的 執行緒物件
// 啟動
// 呼叫 join
// 將所有結果聚合
// 輸出一下
// 執行緒物件執行的內容
//
以上代碼中,FlightQuery 提供了一個回傳方法,學到這里大家應該注意到了,不管 是 Thread 的 run 方法,還是 Runnable 介面,都是 void 回傳型別,如果你想通過某個線 程的運行得到結果,就需要自己定義一個回傳介面, 査詢 Flight 的 task,其實就是一個執行緒的子類,主要用于到各大航空公司獲取資料, 結合老師滴示例代碼,我寫的代碼如下
package com.bjsxt.chapter04.demo08;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
public class FlightQueryTask extends Thread implements FlightQuery {
private String origin ;
private String destination;
// 航班資訊資料 不能加static!!!否則結果出錯,因為加了他們取的村的是同一地址的資料
private final List<String> flightList = new ArrayList<>();
FlightQueryTask(String airline,String origin,String destination){
super("[" + airline + "]");
this.origin = origin;
this.destination = destination;
}
@Override
public void run() {
System.out.printf("query %s from %s to %s\n",this.getName(),origin,destination);
int randomVal = ThreadLocalRandom.current().nextInt(10);
try{
TimeUnit.SECONDS.sleep(randomVal);
}catch (Exception e){
e.printStackTrace();
}
flightList.add(this.getName()+"-"+randomVal);
System.out.printf("The flight :%s list query successfully\n",this.getName());
}
// 回傳航班資料
@Override
public List<String> get() {
return this.flightList;
}
}
介面定義好了,查詢航班資料的執行緒也有了,下面就來實作一下從 SH(上海)到 BJ(北 京)的航班查詢吧,結合老師的示例,我自己寫的代碼如下:
package com.bjsxt.chapter04.demo08;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;
public class FlightQueryExample {
// 與 app 合作的航空公司
private static List<String> airlineList = Arrays.asList("東方航空","南方航空","海南航空");
public static void main(String[] args) {
List<String> result = search("BJ","SH");
System.out.println("---------query result----------");
result.forEach(System.out::println);
}
private static List<String> search(String origin,String destination){
List<String> result = new ArrayList<>();
// 對每個航空公司查詢一次
List<FlightQueryTask> tasks = airlineList.stream()
.map(f -> new FlightQueryTask(f,origin,destination))
.collect(Collectors.toList());
// 啟動執行緒
tasks.forEach(Thread::start);
// 然后呼叫 join
for (FlightQueryTask task : tasks) {
try{
task.join();
}catch (InterruptedException e){
e.printStackTrace();
}
}
// 獲取結果
for (FlightQueryTask task : tasks) {
result.addAll(task.get());
}
return result;
}
}
上面的代碼,關鍵的地方已通過注釋解釋得非常清楚,主執行緒收到了 search 請求之后, 交給了若干個査詢執行緒分別進行作業,最后將每一個執行緒獲取的航班資料進行統一的匯總, 由于每個航空公司的查詢時間可能不一樣,所以用了一個隨機值來反應不同的查詢速度,返 回給客戶端(列印到控制臺),程式的執行結果輸出如下:

4.8、如何關閉一個執行緒
JDK 有一個 Deprecated 方法 stop,但是該方法存在一個問題,JDK 官方早已經不推 薦使用,其在后面的版本中有可能會被移除,根據官網的描述,該方法在關閉執行緒時可能不 會釋放掉 monitor 的鎖,所以強烈建議不要使用該方法結束執行緒,本節將主要介紹幾種關 閉執行緒的方法,
4.8.1、正常關閉
A. 執行緒結束生命周期正常結束
執行緒運行結東,完成了自己的使命之后,就會正常退出,如果執行緒中的任務耗時比較短, 或者時間可控,那么放任它正常結束就好了,
B. 捕獲中斷信號關閉執行緒
我們通過 new Thread 的方式創建執行緒,這種方式看似很簡單,其實它的派生成本是比 較高的,因此在一個執行緒中往往會回圈地執行某個任務,比如心跳檢査,不斷地接收網路消 息報文等,系統決定退出的時候,可以借助中斷執行緒的方式使其退出,示例代碼如下
package com.bjsxt.chapter04.demo09;
import java.util.concurrent.TimeUnit;
public class InterruptThreadExit {
public static void main(String[] args)throws InterruptedException{
Thread t = new Thread("t"){
@Override
public void run() {
System.out.println("I will start work.");
while(!this.isInterrupted()){
// work
}
System.out.println("I will be exiting.");
}
};
t.start();
TimeUnit.SECONDS.sleep(5);
System.out.println("Main will be exiting.");
t.interrupt();
}
}
運行結果

上面的代碼是通過檢査執行緒 interrupt 的標識來決定是否退出的,如果在執行緒中執行 某個可中斷方法,則可以通過捕獲中斷信號來決定是否退出,
C. 使用 volatile 開關控制
由于執行緒的 interrupt 標識很有可能被擦除,或者邏輯單元中不會呼叫任何可中斷方 法,所以使用 volatile 修飾的開關 flag 關閉執行緒也是一種常用的做法,具體如下:
package com.bjsxt.chapter04.demo09;
import java.util.concurrent.TimeUnit;
public class InterruptThreadExit {
public static void main(String[] args)throws InterruptedException{
Thread t = new Thread("t"){
@Override
public void run() {
System.out.println("I will start work.");
while(!this.isInterrupted()){
// work
}
System.out.println("I will be exiting.");
}
};
t.start();
TimeUnit.SECONDS.sleep(5);
System.out.println("Main will be exiting.");
t.interrupt();
}
}
運行結果

上面的例子中定義了一個 closed 開關變數,并且是使用 volatile 修飾(關于 volatile 關鍵字會在后面進行非常細致地講解, *volatile 關鍵字在 Java 中是一個革命 性的關鍵字,非常重要,它是 Java 原子變數以及并發包的基礎),*運行上面的程式同樣也 可以關閉執行緒,
4.8.2、例外退出
在一個執行緒的執行單元中,是不允許拋出 checked 例外的,不論 Thread 中的 run 方 法,還是 Runnable 中的 run 方法,如果執行緒在運行程序中需要捕獲 checked 例外并且判 斷是否還有運行下去的必要,那么此時可以將 checked 例外封裝成 unchecked 例外 (RuntimeException)拋出進而結束執行緒的生命周期
4.8.3、行程假死
相信很多程式員都會遇到行程假死的情況,所謂假死就是行程雖然存在,但沒有日志輸 出,程式不進行任何的作業,看起來就像死了一樣,但事實上它是沒有死的,程式之所以出 現這樣的情況,絕大部分的原因就是某個執行緒阻塞了,或者執行緒出現了死鎖的情況,
我們需要借助一些工具來幫助診斷,比如 jstack、 jconsole、 jvisualvm 等工具, 在本節中,這一節簡單介紹一下 jvisualvm 這個可視化工具,后面我們還會接觸這些工具進行死鎖的判斷等操作, IntelliJ IDEA 其實也是一個 Java 行程,打開 jvisualvm,選擇 IntelliJ IDEA 行程,如下圖所示,將右側的 Tab 切換到【執行緒】,


如果行程無法退出,則會出現假死的情況,可以打開 jvisualvm 查看有哪些活躍執行緒 它們的狀態是什么,該執行緒在呼叫哪個方法而進入了阻塞,
4.9、總結
在本章中,我們比較詳細地學習了 Thread 的大多數 API,其中有獲取執行緒資訊的方法, 如 getId() 、 getName() 、 getPriority() 、 currentThread(), 也 有 阻 塞 方 法 sleep()、 join()方法等,并且結合若干個實戰例子幫助大家更好地理解相關的 API, Thread 的 API 是掌握高并發編程的基礎,因此非常有必要熟練掌握,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/259221.html
標籤:java
