多線程源碼明白了嗎?不明白的話來看騰訊大牛給你畫的面試重點

叮鈴鈴~java

誰啊,大早上的擾人清夢,不知道好不容易有個週末嗎?接電話知道是朋友約我出去釣魚,順便跟我聊一下前段時間讓他幫忙內推一下我小侄子去實習的事情面試

見面以後,他直接開門見山,小侄子在面試的時候表現不錯,最後一面是他來面的,問的至關深刻,侄子表現也不錯,可是在多線程這個地方,他稍微問的深刻了一些,小朋友明顯的慌張起來,不少知識點回答的至關很差(我說這小子怎麼面試回來以後都不敢找我問問題了),朋友說問題不大,這裏他不會卡他,過幾天應該就能收到offer了服務器

雖然朋友這樣說,可是內心卻記下了,多線程這一塊該給這小子補習一下了網絡

我的公衆號:Java架構師聯盟,每日更新技術好文呢多線程


其實多線程應該是如今不少朋友的難點吧,沒什麼時間看源碼,工做的時候又不多能應用獲得,可是在面試的時候,這一塊又是面試的重點,那應該怎麼辦呢?其實不須要慌張,由於面試也算是考試,考試就會劃重點,我總共總結出如下幾點,你們能夠先看一下能詳細的回想起來多少架構

線程sleep併發

線程yielddom

設置線程的優先級獲取線程IDide

獲取當前線程函數

設置線程上下文類加載器

線程interrupt

線程join

如何關閉—個線程

若是你能回答出來80%以上,那基本就沒什麼問題了,否則,你可能要仔細的往下看,有的朋友會說我還年輕,不須要這個,那我要告訴你,這些知識點確實可能不會阻擋你拿offer,可是,會影響你後期的發展速度以及在公司享受的資源,不信就去問你老大

好了,話很少說,咱們來看一下從源碼角度,這些問題你都該回答那些東西

線程 sleep

sleep是一個靜態方法,其有兩個重載方法,其中一個須要傳入毫秒數,另一個既需 要毫秒數也須要納秒數。

sleep方法介紹

 public static void sleep(long millis) throws InterruptedException
 public static void sleep(long millis, int nanos) throws InterruptedException

sleep方法會使當前線程進入指定毫秒數的休眠,暫停執行,雖然給定了一個休眠的 時間,可是最終要以系統的定時器和調度器的精度爲準,休眠有一個很是重要的特性, 那就是其不會放棄monitor鎖的全部權(在後文中講解線程同步和鎖的時候會重點介紹 monitor),下面咱們來看一個簡單的例子:

package com.wangwenjun,concurrent,chapter03;
public class Threadsleep
public static void main(String[] args)
new Thread(()->


long startTime = System.currentTimeMillis();
sleep(2_000L);
long endTime = System.currentTimeMillis();
System.out.printin(String.format("Total spend %d ms", (endTime - startTime)));
}).start();
long startTime = System.currentTimeMillis();
sleep(3_000L);
long endTime = System.currentTimeMillis();
System.out.printin(String.format("Main thread total spend %d ms", (endTime - startTime)));
}
private static void sleep(long ms)
{
try
{
Thread.sleep(ms);
} catch (InterruptedException e)
{
}
}
}

在上面的例子中,咱們分別在自定義的線程和主線程中進行了休眠,每一個線程的休眠 互不影響,Thread.sleep只會致使當前線程進入指定時間的休眠。

使用 TimeUnit 替代 Thread.sleep

在JDK1.5之後,JDK引入了一個枚舉TimeUnit,其對sleep方法提供了很好的封裝, 使用它能夠省去時間單位的換算步驟,好比線程想休眠3小時24分17秒88毫秒,使用 TimeUnit來實現就很是的簡便優雅了 :

Thread.sleep(12257088L);
TimeUnit.HOURS.sleep(3);
TimeUnit・ MINUTES.sleep(24);
TimeUnit, SECONDS・ sleep(17);
TimeUnit.MILLISECONDS.sleep(88);

一樣的時間表達,TimeUnit顯然清晰不少,筆者強烈建議,在使用Thread.sleep的地 方,徹底使用TimeUnit來代替,由於sleep能作的事,TimeUnit所有都能完成,而且功能 更加的強大,在本書後面的內容中,我將所有采用TimeUnit替代sleep。

線程 yield

yield方法介紹

yield方法屬於一種啓發式的方法,其會提醒調度器我願意放棄當前的CPU資源,若是 CPU的資源不緊張,則會忽略這種提醒。

調用yield方法會使當前線程從RUNNING狀態切換到RUNNABLE狀態,通常這個方 法不太經常使用:

package com.wangwenjun.concurrent,chapter03;
import java.util.stream.IntStream;
public class ThreadYield
{
public static void main(String[] args)
{
IntStream.range(0, 2).mapToObj(ThreadYield::create)
.forEach(Thread::start);
}
private static Thread create(int index)
{
return new Thread(() ->
{
//①註釋部分
//if (index == 0)
// Thread, yield();
System.out.printin(index);
});
}
}

上面的程序運行不少次,你會發現輸出的結果不一致,有時候是0最早打印出來,有

時候是1最早打印出來,可是當你打開代碼的註釋部分,你會發現,順序始終是0, 10 由於第一個線程若是最早得到了 CPU資源,它會比較謙虛,主動告訴CPU調度器是 放了本來屬於本身的資源,可是yield R是一個提示(hint), CPU調度器並不會擔保每次都 能知足yield提示。

yield sleep

看過前面的內容以後,會發現yield和sleep有一些混淆的地方,在JDK1.5之前的版本 中yield的方法事實上是調用了 sleep(O),可是它們之間存在着本質的區別,具體以下。

□ sleep會致使當前線程暫停指定的時間,沒有CPU時間片的消耗。

□ yield只是對CPU調度器的一個提示,若是CPU調度器沒有忽略這個提示,它會導

致線程上下文的切換。

□ sleep會使線程短暫block,會在給定的時間內釋放CPU資源。

□ yield會使RUNNING狀態的Thread進入RUNNABLE狀態(若是CPU調度器沒有 忽略這個提示的話)。

□ sleep幾乎百分之百地完成了給定時間的休眠,而yield的提示並不能必定擔保。

□ 一個線程sleep另外一個線程調用interrupt會捕獲到中斷信號,而yield則不會。

設置線程的優先級

□ public final void setPriority(int newPriority)爲線程設定優先級。

□ public final int getPriority()獲取線程的優先級。

線程優先級介紹

進程有進程的優先級,線程一樣也有優先級,理論上是優先級比較高的線程會獲取優 先被CPU調度的機會,可是事實上每每並不會如你所願,設置線程的優先級一樣也是一個 hint操做,具體以下。

□對於root用戶,它會hint操做系統你想要設置的優先級別,不然它會被忽略。

□若是CPU比較忙,設置優先級可能會得到更多的CPU時間片,可是閒時優先級的 高低幾乎不會有任何做用。

因此,不要在程序設計當中企圖使用線程優先級綁定某些特定的業務,或者讓業務嚴 重依賴於線程優先級,這可能會讓你大失所望。舉個簡單的例子,可能不一樣狀況下的運行 效果不會徹底同樣,可是咱們只是想讓優先級比較高的線程得到更多的信息輸出機會,示 例代碼以下:

package com.wangwenjun.concurrent.chapter03;
public class Threadpriority
{
public static void main(String[] args)
{
Thread tl = new Thread(()->
{
while (true)
{
System.out.printin("tl");
}
});
tl・ setPriority(3);


Thread t2 = new Thread(()->
{
while (true)
{
System. out. printin (,lt2");
}
});
t2.setPriority(10);
tl.start();
t2.start();
}
}

運行上面的程序,會發現t2出現的頻率很明顯要高一些,固然這也和筆者當前CPU的 資源狀況有關係,不一樣狀況下的運行會有不同的結果。

線程優先級源碼分析

設置線程的優先級,只須要調用setPriority方法便可,下面咱們打開Thread的源碼, 一塊兒來分析一下:

public final void setPriority(int newPriority) {
ThreadGroup g; checkAccess(); if (newPriority > MAX_PRIORITY || newPriority < MIN_PRIORITY) { throw new IllegalArgumentException();
}
if((g = getThreadGroup()) 1= null) {
if (newPriority > g.getMaxPriority()) { newPriority = g.getMaxPriority();
}
setPriorityO(priority = newPriority);
}
}

經過上面源碼的分析,咱們能夠看出,線程的優先級不能小於1也不能大於10,若是 指定的線程優先級大於線程所在group的優先級,那麼指定的優先級將會失效,取而代之 的是group的最大優先級,下面咱們經過一個例子來證實一下:

package com.wangwenjun.concurrent.chapter03;
public class Threadpriority
{
public static void main(String[] args)
{
//定義一個線程組
ThreadGroup group = new ThreadGroup("test");
//將線程組的優先級指定爲7
group.setMaxPriority(7);
//定義一個線程,將該線程加入到group中
Thread thread = new Thread(group, "test-thread");
//企圖將線程的優先級設定爲10
thread.setPriority(10);
/ /企圖未遂
System.out.printin(thread.getPriority());
}
}

上面的結果輸出爲7,而不是10,由於它超過了所在線程組的優先級別

關於優先級的一些總結

通常狀況下,不會對線程設定優先級別,更不會讓某些業務嚴重地依賴線程的優先級 別,好比權重,藉助優先級設定某個任務的權重,這種方式是不可取的,通常定義線程的 時候使用默認的優先級就行了,那麼線程默認的優先級是多少呢?

線程默認的優先級和它的父類保持一致,通常狀況下都是5,由於main線程的優先級 就是5,因此它派生出來的線程都是5,示例代碼以下:

package com.wangwenjun.concurrent.chapter03;
public class Threadpriority
{
public static void main(String[] args)
{
Thread tl = new Thread();
System.out,println("tl priority " + tl.getPriority());
Thread t2 = new Thread(()->
{
Thread t3 = new Thread();
System.out.printIn("t3 priority " + t3.getPriority());
});
t2 ・ setPriority(6);
t2 ・ start();
System.out.printin("t2 priority " + t2.getPriority());

上面程序的輸出結果是tl的優先級爲5,由於main線程的優先級是5 ; t2的優先級 是6,由於顯式地將其指定爲6; t3的優先級爲6,沒有顯式地指定,所以其與父線程保持 一致。

獲取線程ID

public long getld()獲取線程的惟一 ID,線程的ID在整個JVM進程中都會是惟一的,

而且是從0開始逐次遞增。若是你在main線程(main函數)中建立了一個惟一的線程,並 且調用getld()後發現其並不等於0,也許你會納悶,不該該是從0開始的嗎?以前已經說 過了在一個JVM進程啓動的時候,其實是開闢了不少個線程,自增序列已經有了必定的 消耗,所以咱們本身建立的線程絕非第0號線程。

獲取當前線程

public static Thread currentThread()用於返回當前執行線程的引用,這個方法雖然很簡 單,可是使用很是普遍,咱們在後面的內容中會大量的使用該方法,來看一段示例代碼:

 package com.wangwenjun.concurrent.chapter03;
public class CurrentThread
{
public static void main(String[] args)
{
Thread thread = new Thread()
{
@Override
public void run()
{
//always true System.out.printin(Thread.CurrentThread() == this);
}
};
thread, start();
String name = Thread.CurrentThread().getName(); System.out.printIn("main".equals(name));
}
}

上面程序運行輸出的兩個結果都是true。

設置線程上下文類加載器

□ public ClassLoader getContextClassLoader()獲取線程上下文的類加載器,簡單來講 就是這個線程是由哪一個類加器加載的,若是是在沒有修改線程上下文類加載器的情 況下,則保持與父線程一樣的類加載器。

□ public void setContextClassLoader(ClassLoader cl)設置該線程的類加載器,這個方法 能夠打破JAVA類加載器的父委託機制,有時候該方法也被稱爲JAVA類加載器的 後門。

關於線程上下文類加載器的內容咱們將在本書的第11章重點介紹,而且結合jdbc驅動 包的源碼分析JDK的開發者爲何要留有這樣的後門。

線程 interrupt

線程interrupt,是一個很是重要的API,也是常用的方法,與線程中斷相關的API 有以下幾個,在本節中咱們也將Thread深刻源碼對其進行詳細的剖析。

□ public void interrupt()
□ public static boolean interrupted()
□ public boolean islnterrupted()

interrupt

以下方法的調用會使得當前線程進入阻塞狀態,而調用當前線程的interrupt方法,就 能夠打斷阻塞。

□ Object 的 wait 方法。
□ Object 的 wait(long)方法。
□ Object 的 wait(long,int)方法。
□ Thread 的 sleep(long)方法。
□ Thread 的 sleep(long,int)方法。
□ Thread 的 join 方法。
□ Thread 的 join(long)方法。
□ Thread 的 join(long,int)方法。
□ InterruptibleChannel 的 io 操做。
□ Selector 的 wakeup 方法。

□其餘方法。

上述若干方法都會使得當前線程進入阻塞狀態,若另外的一個線程調用被阻塞線程的 interrupt方法,則會打斷這種阻塞,所以這種方法有時會被稱爲可中斷方法,.記住,打斷一 個線程並不等於該線程的生命週期結束,僅僅是打斷了當前線程的阻塞狀態。

一旦線程在阻塞的狀況下被打斷,都會拋出一個稱爲InterruptedException的異常,這 個異常就像一個signal (信號)同樣通知當前線程被打斷了,下面咱們來看一個例子:

package com.wangwenjun,concurrent,chapter03;
import java.util.concurrent .TimeUnit;
public class Threadinterrupt
public static void main(String[] args) throws InterruptedException
{
Thread thread = new Thread(()->
{
try
{
TimeUnit•MINUTES.sleep(1);
} catch (InterruptedException e)
{
System.out.printIn("Oh, i am be interrupted.");
} 、
});
thread.start();
//short block and make sure thread is started.
TimeUnit, MILLISECONDS.sleep(2);
thread, interrupt();
}
}

上面的代碼建立了一個線程,而且企圖休眠1分鐘的時長,不過很惋惜,大約在2毫 秒以後就被主線程調用interrupt方法打斷,程序的執行結果就是「Oh, i am be interrupted.」

interrupt這個方法到底作了什麼樣的事情呢?在一個線程內部存在着名爲interrupt flag 的標識,若是一個線程被interrupt,那麼它的flag將被設置,可是若是當前線程正在執行 可中斷方法被阻塞時,調用interrupt方法將其中斷,反而會致使flag被清除,關於這點我 們在後面還會作詳細的介紹。另外有一點須要注意的是,若是一個線程已是死亡狀態, 那麼嘗試對其的interrupt會直接被忽略。

islnterrupted

islnterrupted是Thread的一個成員方法,它主要判斷當前線程是否被中斷,該方法僅 僅是對interrupt標識的一個判斷,並不會影響標識發生任何改變,這個與咱們即將學習到 的interrupted是存在差異的,下面咱們看一個簡單的程序:

package com.wangwenjun,concurrent•chapter03;
import java.util.concurrent.TimeUnit;
public class ThreadisInterrupted
{
public static void main(String[] args) throws InterruptedException
Thread thread = new Thread()
{
@Override
public void run()
{
while (true)
{
//do nothing, just empty loop.
}
}
};
thread.start();
TimeUnit.MILLISEC0NDS.sleep(2);
System.out.printf("Thread is interrupted ? %s\n", thread.isInterrupted()); thread.interrupt();
System.out.printf("Thread is interrupted ? %s\n", thread.islnterrupted());
}
}

上面的代碼中定義了一個線程,而且在線程的執行單元中(run方法)寫了一個空的死 循環,爲何不寫sleep呢?由於sleep是可中斷方法,會捕獲到中斷信號,從而干擾咱們 程序的結果。下面是程序運行的結果,記得手動結束上面的程序運行,或者你也能夠將上 面定義的線程指定爲守護線程,這樣就會隨着主線程的結束致使JVM中沒有非守護線程而 自動退出。

Thread is interrupted ? false
Thread is interrupted ? true

可中斷方法捕獲到了中斷信號(signal)以後,也就是捕獲了 InterruptedException異常 以後會擦除掉interrupt的標識,對上面的程序稍做修改,你會發現程序的結果又會出現很 大的不一樣,示例代碼以下:

package com.wangwenjun.concurrent.chapter03;
import java.util.concurrent.TimeUnit;
public class ThreadisInterrupted
{
public static void main(String[] args) throws InterruptedException
{
Thread thread = new Thread()
{
@Override
public void run()
while (true)


try
{
TimeUnit.MINUTES.sleep(1);
} catch (InterruptedException e)
{
//ignore the exception
//here the interrupt flag will be clear.
System.out.printf("I am be interrupted ? %s\n", islnterrupted());
}
}
}
};
thread.setDaemon(true);
thread.start();
TimeUnit.MILLISECONDS.sleep(2);
System.out.printf("Thread is interrupted ? %s\n", thread.isInterrupted()); thread.interrupt();
TimeUnit, MILLISECONDS・ sleep(2); System.out.printf("Thread is interrupted ? %s\n", thread.isInterrupted());
}
}

因爲在run方法中使用了 sleep這個可中斷方法,它會捕獲到中斷信號,而且會擦除 interrupt標識,所以程序的執行結果都會是false,程序輸岀以下:

Thread is interrupted ? false
I am be interrupted ? false
Thread is interrupted ? false

其實這也不難理解,可中斷方法捕獲到了中斷信號以後,爲了避免影響線程中其餘方法 的執行,將線程的interrupt標識復位是一種很合理的設計。

interrupted

interrupted是一個靜態方法,雖然其也用於判斷當前線程是否被中斷,可是它和成員方 法islnterrupted仍是有很大的區別的,調用該方法會直接擦除掉線程的interrupt標識,需 要注意的是,若是當前線程被打斷了,那麼第一次調用interrupted方法會返回true,而且 當即擦除了 interrupt標識;第二次包括之後的調用永遠都會返回false,除非在此期間線程 又一次地被打斷,下面設計了一個簡單的例子,來驗證咱們的說法:

package com.wangwenjun.concurrent.chapter03;
import java.util.concurrent.TimeUnit;
public class Threadinterrupted
public static void main(String[] args) throws InterruptedException
{
Thread thread = new Thread()
{
@Override
public void run()
{
while (true)
{
System.out.printIn(Thread.interrupted());
}
}
};
thread.setDaemon(true);
thread.start();
//shortly block make sure the thread is started.
TimeUnit, MILLISECONDS.sleep(2); thread.interrupt();
}
}

一樣因爲不想要受到可中斷方法如sleep的影響,在Thread的run方法中沒有進行任 何短暫的休眠,因此運行上面的程序會出現很是多的輸出,可是咱們經過對輸出的檢查會 發現以下所示的內容,其足以做爲對該方法的解釋。

false
false
true
false
false

在不少的false包圍中發現了一個true,也就是interrupted方法判斷到了其被中斷,立 即擦除了中斷標識,而且只有這一次返回true,後面的都將會是false。

interrupt 注意事項

打開Thread的源碼,不難發現,islnterrupted方法和interrupted方法都調用了同一個 本地方法:

private native boolean islnterrupted(boolean Clearlnterrupted);

其中參數Clearlnterrupted主要用來控制是否擦除線程interrupt的標識。

islnterrupted方法的源碼中該參數爲false,表示不想擦除:

public boolean islnterrupted() { return islnterrupted(false);
}

而interrupted靜態方法中該參數則爲true,表示想要擦除:

public static boolean interrupted() {
return currentThread().islnterrupted(true);
}

在比較詳細地學習了 interrupt方法以後,你們思考一個問題,若是一個線程在沒有執 行可中斷方法以前就被打斷,那麼其接下來將執行可中斷方法,好比sleep會發生什麼樣的 狀況呢?下面咱們經過一個簡單的實驗來回答這個疑問:

public static void main(String!] args)
{
//① 判斷當前線程是否被中斷
System.out.printin("Main thread is interrupted? " + Thread.interrupted());
//②中斷當前線程
Thread.currentThread().interrupt();
//③判斷當前線程是否已經被中斷
System.out.printin("Main thread is interrupted? " + Thread.currentThread(). islnterrupted());
try
{
//④ 當前線程執行可中斷方法 TimeUnit.MINUTES.sleep(1);
} catch (InterruptedException e)
{
//⑤捕獲中斷信號
System.out.printin("I will be interrupted still.");
}
}

經過運行上面的程序,你會發現,若是一個線程設置了 interrupt標識,那麼接下來的 可中斷方法會當即中斷,所以註釋⑤的信號捕獲部分代碼會被執行,請你們注意註釋①和注 釋③中判斷線程中斷方法的不一樣,也但願讀者結合本節的內容思考爲何要這麼作?

線程 join

Thread的join方法一樣是一個很是重要的方法,使用它的特性能夠實現不少比較強大 的功能,與sleep -樣它也是一個可中斷的方法,也就是說,若是有其餘線程執行了對當前 線程的interrupt操做,它也會捕獲到中斷信號,而且擦除線程的interrupt標識,Thread的 API爲咱們提供了三個不一樣的join方法,具體以下。

□ public final void join() throws InterruptedException
□ public final synchronized void join(long millis, int nanos)
throws InterruptedException
□ public final synchronized void join(long millis)
throws InterruptedException

在本節中,筆者將會詳細介紹join方法以及如何在實際應用中使用join方法。

線程join方法詳解

join某個線程A,會使當前線程B進入等待,直到線程A結束生命週期,或者到達給 定的時間,那麼在此期間B線程是處於BLOCKED的,而不是A線程,下面就來經過一個 簡單的實例解釋一下join方法的基本用法:

package com.wangwenjun.concurrent.chapter03;
import java.util.List;
import java.util.concurrent.TimeUnit;
import java.util.stream.IntStream;
import static java.util.stream.Collectors.toList;
public class Threadjoin
{
public static void main(String[] args) throws InterruptedException
{
//①定義兩個線程,並保存在threads中
List<Thread> threads = IntStream.range(1, 3)
.mapToObj(Threadjoin::create).collect(toList());
//②啓動這兩個線程
threads•forEach(Thread::start);
//③ 執行這兩個線程的join方法
for (Thread thread : threads)
{
thread.join();
}
//④main線程循環輸出
for (int i = 0; i < 10; i++)
System.out.printin(Thread.currentThread().getName() + "+ i); shortSleep();


//構造一個簡單的線程,每一個線程只是簡單的循環輸出
private static Thread create(int seq)
{
return new Thread(() ->
{
for (int i = 0; i < 10; i++)
{
System・ out・ printin(Thread.currentThread()・ getName() + "#" + i); shortSleep();
}
}, String.valueOf(seq));
}
private static void shortSleep()
{
try
{
TimeUnit, SECONDS.sleep(1);
} catch (InterruptedException e)
{
e.printStackTrace();
}
}
}

上面的代碼結合Java 8的語法,建立了兩個線程,分別啓動,而且調用了每一個線程的 join方法(注意:join方法是被主線程調用的,所以在第一個線程尚未結束生命週期的時 後,第二個線程的join不會獲得執行,可是此時,第二個線程也已經啓動了),運行上面的 程序,你會發現線程一和線程二會交替地輸出直到它們結束生命週期,main線程的循環才 會開始運行,程序輸岀以下:

2#8
1#8
2#9
1#9
main#0
main#l
main#2
main#3

若是你將註釋③下面的join所有註釋掉,那麼三個線程將會交替地輸出,程序輸出以下:

main#2
2#2
1#2
main#3
1#3
2#3 main#4

join方法會使當前線程永遠地等待下去,直到期間被另外的線程中斷,或者join的線 程執行結束,固然你也可使用join的另外兩個重載方法,指定毫秒數,在指定的時間到 達以後,當前線程也會退出阻塞。一樣思考一個問題,若是一個線程已經結束了生命週期, 那麼調用它的join方法的當前線程會被阻塞嗎?

join方法結合實戰

本節咱們將結合一個實際的案例,來看一下join方法的應用場景,假設你有一個APP, 主要用於查詢航班信息,你的APP是沒有這些實時數據的,當用戶發起查詢請求時,你需 要到各大航空公司的接口獲取信息,最後統一整理加工返回到APP客戶端,如圖3-1所示, 固然JDK自帶了不少高級工具,好比CountDownLatch和CyclicBarrier等均可以完成相似 的功能,可是僅就咱們目前所學的知識,使用join方法便可完成下面的功能。

多線程源碼明白了嗎?不明白的話來看騰訊大牛給你畫的面試重點

 

該例子是典型的串行任務局部並行化處理,用戶在APP客戶端輸入出發地「北京」和 目的地「上海」,服務器接收到這個請求以後,先來驗證用戶的信息,而後到各大航空公司 的接口查詢信息,最後通過整理加工返回給客戶端,每個航空公司的接口不會都同樣, 獲取的數據格式也不同,查詢的速度也存在着差別,若是再跟航空公司進行串行化交互 (逐個地查詢),很明顯客戶端須要等待很長的時間,這樣的話,用戶體驗就會很是差。若是 咱們將每個航空公司的查詢都交給一個線程去工做,而後在它們結束工做以後統一對數 據進行整理,這樣就能夠極大地節約時間,從而提升用戶體驗效果。

代碼清單3-1 查詢接口 FightQuery

package com.wangwenjun.concurrent.chapter03;
import j ava.util.List;
public interface FightQuery
{
List<String> get();
}

在代碼清單3-1中,FightQuery提供了一個返回方法,寫到這裏你們應該注意到了,不 管是Thread的run方法,仍是Runnable接口,都是void返回類型,若是你想經過某個線 程的運行獲得結果,就須要本身定義一個返回的接口。

查詢Fight的task,其實就是一個線程的子類,主要用於到各大航空公司獲取數據,示 例代碼以下:

package com.wangwenjun,concurrent.chapter03;
import java.util.ArrayList;
import j ava.util.List;
import j ava.util.concurrent.ThreadLocalRandom;
import java.util.concurrent.TimeUnit;
public class FightQueryTask extends Thread implements FightQuery

 

public FightQueryTask(String airline, String origin, String destination)
super("[" + airline + "]"); this.origin = origin;
this.destination = destination; }
@Override public void run()
{
System.out.printf("%s-query from %s to %s \n", getName(), origin, destination);
int randomVal = ThreadLocalRandom.current().nextlnt(10);
try
{
TimeUnit.SECONDS.sleep(randomVal);
this.flightList.add(getName() + + randomVal);
System.out.printf("The Fight:%s list query successful\n", getName());
} catch (InterruptedException e)
{
}
}
@Override
public List<String> get()
{
return this.flightList;
}
}

接口定義好了,查詢航班數據的線程也有了,下面就來實現一下從SH (上海)到北京 (BJ)的航班查詢吧!示例代碼以下:

package com.wangwenjun.concurrent.chapter0 3;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import static java.util.stream.Collectors.toList;
public class FightQueryExample
{
//①合做的各大航空公司
private static List<String> fightCompany = Arrays.asList(
"CSA", "CEA", "HNA"
);
public static void main(String[] args)
{
List<String> results = search("SH", "BJ");
System.out.printin("===========result===========");
results , forEach(System•out::printin);
}
private static List<String> search(String original, String dest)
{
final List<String> result = new ArrayList<>();
//②建立查詢航班信息的線程列表
List<FightQueryTask> tasks = fightCompany.stream()
.map(f -> createSearchTask(f, original, dest))
.collect(toList());
//③分別啓動這幾個線程
tasks , forEach(Thread::start);

〃④分別調用每個線程的join方法,阻塞當前線程 tasks.forEach(t ->
try
t.join();
} catch (InterruptedException e)
{
}
});
//⑤在此以前,當前線程會阻塞住,獲取每個查詢線程的結果,而且加入到result中
tasks.stream().map(FightQuery::get).forEach(result::addAll);
return result;
}
FightQueryTask createSearchTask( fight,
original, String dest)
return new FightQueryTask(fight, original, dest);
}

上面的代碼,關鍵的地方已經過註釋解釋得很是清楚,主線程收到了 search請求以後, 交給了若干個查詢線程分別進行工做,最後將每個線程獲取的航班數據進行統一的彙總。 因爲每一個航空公司的查詢時間可能不同,因此用了一個隨機值來反應不一樣的查詢速度, 返回給客戶端(打印到控制檯),程序的執行結果輸出以下:

[CSA]-query from SH to BJ [CEA]-query from SH to BJ [HNA]-query from SH to BJ The Fight:[HNA] list query The Fights[CSA] list query The Fight:[CEA] list query ===========result=========: [CSA]-4
[CEA]-7
[HNA]-2

如何關閉一•個線程

JDK有一個Deprecated方法stop,可是該方法存在一個問題,JDK官方早已經不推薦 使用,其在後面的版本中有可能會被移除,根據官網的描述,該方法在關閉線程時可能不

 

會釋放掉monitor的鎖,因此強烈建議不要使用該方法結束線程,本節將主要介紹幾種關閉 線程的方法。

正常關閉

\1. 線程結束生命週期正常結束

線程運行結束,完成了本身的使命以後,就會正常退出,若是線程中的任務耗時比較 短,或者時間可控,那麼聽任它正常結束就行了。

\2. 捕獲中斷信號關閉線程

咱們經過new Thread的方式建立線程,這種方式看似很簡單,其實它的派生成本是比 較高的,所以在一個線程中每每會循環地執行某個任務,好比心跳檢查,不斷地接收網絡 消息報文等,系統決定退出的時候,能夠藉助中斷線程的方式使其退出,示例代碼以下:

package com.wangwenjun.concurrent.chapter0 3;
import java.util.concurrent. TimeUnit;
public class InterruptThreadExit
{
public static void main(String[] args) throws InterruptedException
{
Thread t = new Thread()
{
@Override
public void run()
{
System.out.printin("I will start work");
while (!islnterrupted())
{
//working.
}
System.out.printin("I will be exiting.");
}
};
t.start();
TimeUnit.MINUTES・ sleep(1); System.out.printin("System will be shutdown."); t.interrupt();
}
}

上面的代碼是經過檢查線程interrupt的標識來決定是否退出的,若是在線程中執行某 個可中斷方法,則能夠經過捕獲中斷信號來決定是否退出。

@Override
public void run()
System.out.printin("I will start work"); for (;;)
//working, try
TimeUnit.MILLISECONDS.sleep(l); catch (InterruptedException e)
break;
}
}
System.out.printin("I will be exiting.");
}

上面的代碼執行結果都會致使線程正常的結束,程序輸出以下:

I will start work
System will be shutdown.
I will be exiting.

\3. 使用volatile開關控制

因爲線程的interrupt標識頗有可能被擦除,或者邏輯單元中不會調用任何可中斷方法, 因此使用volatile修飾的開關flag關閉線程也是一種經常使用的作法,具體以下:

package com.wangwenjun.concurrent.chapter03;
import java.util•concurrent.TimeUnit;
public class FlagThreadExit
static class MyTask extends Thread
private volatile boolean closed = false;
@Override public void run()
System.out.printIn("I will start work"); while (Iclosed && !islnterrupted())
//正在運行
}
System.out.printin("I will be exiting.");
public void close()
{
this.closed = true;
this , interrupt();
}
?
public static void main(String[] args) throws InterruptedException
{
MyTask t = new MyTask();
t.start();
TimeUnit・ MINUTES.sleep(1);
System.out.printIn("System will be shutdown.");
t.close();
}
}

上面的例子中定義了一個closed開關變量,而且是使用volatile修飾(關於volatile關 鍵字會在本書的第3部分中進行很是細緻地講解,volatile關鍵字在Java中是一個革命性的 關鍵字,很是重要,它是Java原子變量以及併發包的基礎)運行上面的程序一樣也能夠關 閉線程。

異常退出

在一個線程的執行單元中,是不容許拋出checked異常的,不論Thread中的run方 法,仍是Runnable中的run方法,若是線程在運行過程當中須要捕獲checked異常而且 判斷是否還有運行下去的必要,那麼此時能夠將checked異常封裝成unchecked異常 (RuntimeException)拋出進而結束線程的生命週期。

相關文章
相關標籤/搜索