溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊(cè)×
其他方式登錄
點(diǎn)擊 登錄注冊(cè) 即表示同意《億速云用戶服務(wù)條款》

什么是多線程源碼

發(fā)布時(shí)間:2021-10-23 15:41:38 來源:億速云 閱讀:135 作者:iii 欄目:編程語(yǔ)言

本篇內(nèi)容主要講解“什么是多線程源碼”,感興趣的朋友不妨來看看。本文介紹的方法操作簡(jiǎn)單快捷,實(shí)用性強(qiáng)。下面就讓小編來帶大家學(xué)習(xí)“什么是多線程源碼”吧!

線程 sleep

sleep是一個(gè)靜態(tài)方法,其有兩個(gè)重載方法,其中一個(gè)需要傳入毫秒數(shù),另外一個(gè)既需 要毫秒數(shù)也需要納秒數(shù)。

sleep方法介紹

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

sleep方法會(huì)使當(dāng)前線程進(jìn)入指定毫秒數(shù)的休眠,暫停執(zhí)行,雖然給定了一個(gè)休眠的 時(shí)間,但是最終要以系統(tǒng)的定時(shí)器和調(diào)度器的精度為準(zhǔn),休眠有一個(gè)非常重要的特性, 那就是其不會(huì)放棄monitor鎖的所有權(quán)(在后文中講解線程同步和鎖的時(shí)候會(huì)重點(diǎn)介紹 monitor),下面我們來看一個(gè)簡(jiǎn)單的例子:

package com.wangwenjun,concurrent,chapter03;public class Threadsleeppublic 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){}}}

在上面的例子中,我們分別在自定義的線程和主線程中進(jìn)行了休眠,每個(gè)線程的休眠 互不影響,Thread.sleep只會(huì)導(dǎo)致當(dāng)前線程進(jìn)入指定時(shí)間的休眠。

使用 TimeUnit 替代 Thread.sleep

在JDK1.5以后,JDK引入了一個(gè)枚舉TimeUnit,其對(duì)sleep方法提供了很好的封裝, 使用它可以省去時(shí)間單位的換算步驟,比如線程想休眠3小時(shí)24分17秒88毫秒,使用 TimeUnit來實(shí)現(xiàn)就非常的簡(jiǎn)便優(yōu)雅了 :

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

同樣的時(shí)間表達(dá),TimeUnit顯然清晰很多,筆者強(qiáng)烈建議,在使用Thread.sleep的地 方,完全使用TimeUnit來代替,因?yàn)閟leep能做的事,TimeUnit全部都能完成,并且功能 更加的強(qiáng)大,在本書后面的內(nèi)容中,我將全部采用TimeUnit替代sleep。

線程 yield

yield方法介紹

yield方法屬于一種啟發(fā)式的方法,其會(huì)提醒調(diào)度器我愿意放棄當(dāng)前的CPU資源,如果 CPU的資源不緊張,則會(huì)忽略這種提醒。

調(diào)用yield方法會(huì)使當(dāng)前線程從RUNNING狀態(tài)切換到RUNNABLE狀態(tài),一般這個(gè)方 法不太常用:

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);});}}

上面的程序運(yùn)行很多次,你會(huì)發(fā)現(xiàn)輸出的結(jié)果不一致,有時(shí)候是0最先打印出來,有

時(shí)候是1最先打印出來,但是當(dāng)你打開代碼的注釋部分,你會(huì)發(fā)現(xiàn),順序始終是0, 10 因?yàn)榈谝粋€(gè)線程如果最先獲得了 CPU資源,它會(huì)比較謙虛,主動(dòng)告訴CPU調(diào)度器是 放了原本屬于自己的資源,但是yield R是一個(gè)提示(hint), CPU調(diào)度器并不會(huì)擔(dān)保每次都 能滿足yield提示。

yield sleep

看過前面的內(nèi)容之后,會(huì)發(fā)現(xiàn)yield和sleep有一些混淆的地方,在JDK1.5以前的版本 中yield的方法事實(shí)上是調(diào)用了 sleep(O),但是它們之間存在著本質(zhì)的區(qū)別,具體如下。

□ sleep會(huì)導(dǎo)致當(dāng)前線程暫停指定的時(shí)間,沒有CPU時(shí)間片的消耗。

□ yield只是對(duì)CPU調(diào)度器的一個(gè)提示,如果CPU調(diào)度器沒有忽略這個(gè)提示,它會(huì)導(dǎo)

致線程上下文的切換。

□ sleep會(huì)使線程短暫block,會(huì)在給定的時(shí)間內(nèi)釋放CPU資源。

□ yield會(huì)使RUNNING狀態(tài)的Thread進(jìn)入RUNNABLE狀態(tài)(如果CPU調(diào)度器沒有 忽略這個(gè)提示的話)。

□ sleep幾乎百分之百地完成了給定時(shí)間的休眠,而yield的提示并不能一定擔(dān)保。

□ 一個(gè)線程sleep另一個(gè)線程調(diào)用interrupt會(huì)捕獲到中斷信號(hào),而yield則不會(huì)。

設(shè)置線程的優(yōu)先級(jí)

□ public final void setPriority(int newPriority)為線程設(shè)定優(yōu)先級(jí)。

□ public final int getPriority()獲取線程的優(yōu)先級(jí)。

線程優(yōu)先級(jí)介紹

進(jìn)程有進(jìn)程的優(yōu)先級(jí),線程同樣也有優(yōu)先級(jí),理論上是優(yōu)先級(jí)比較高的線程會(huì)獲取優(yōu) 先被CPU調(diào)度的機(jī)會(huì),但是事實(shí)上往往并不會(huì)如你所愿,設(shè)置線程的優(yōu)先級(jí)同樣也是一個(gè) hint操作,具體如下。

□對(duì)于root用戶,它會(huì)hint操作系統(tǒng)你想要設(shè)置的優(yōu)先級(jí)別,否則它會(huì)被忽略。

□如果CPU比較忙,設(shè)置優(yōu)先級(jí)可能會(huì)獲得更多的CPU時(shí)間片,但是閑時(shí)優(yōu)先級(jí)的 高低幾乎不會(huì)有任何作用。

所以,不要在程序設(shè)計(jì)當(dāng)中企圖使用線程優(yōu)先級(jí)綁定某些特定的業(yè)務(wù),或者讓業(yè)務(wù)嚴(yán) 重依賴于線程優(yōu)先級(jí),這可能會(huì)讓你大失所望。舉個(gè)簡(jiǎn)單的例子,可能不同情況下的運(yùn)行 效果不會(huì)完全一樣,但是我們只是想讓優(yōu)先級(jí)比較高的線程獲得更多的信息輸出機(jī)會(huì),示 例代碼如下:

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();}}

運(yùn)行上面的程序,會(huì)發(fā)現(xiàn)t2出現(xiàn)的頻率很明顯要高一些,當(dāng)然這也和筆者當(dāng)前CPU的 資源情況有關(guān)系,不同情況下的運(yùn)行會(huì)有不一樣的結(jié)果。

線程優(yōu)先級(jí)源碼分析

設(shè)置線程的優(yōu)先級(jí),只需要調(diào)用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);}}

通過上面源碼的分析,我們可以看出,線程的優(yōu)先級(jí)不能小于1也不能大于10,如果 指定的線程優(yōu)先級(jí)大于線程所在group的優(yōu)先級(jí),那么指定的優(yōu)先級(jí)將會(huì)失效,取而代之 的是group的最大優(yōu)先級(jí),下面我們通過一個(gè)例子來證明一下:

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

上面的結(jié)果輸出為7,而不是10,因?yàn)樗^了所在線程組的優(yōu)先級(jí)別

關(guān)于優(yōu)先級(jí)的一些總結(jié)

一般情況下,不會(huì)對(duì)線程設(shè)定優(yōu)先級(jí)別,更不會(huì)讓某些業(yè)務(wù)嚴(yán)重地依賴線程的優(yōu)先級(jí) 別,比如權(quán)重,借助優(yōu)先級(jí)設(shè)定某個(gè)任務(wù)的權(quán)重,這種方式是不可取的,一般定義線程的 時(shí)候使用默認(rèn)的優(yōu)先級(jí)就好了,那么線程默認(rèn)的優(yōu)先級(jí)是多少呢?

線程默認(rèn)的優(yōu)先級(jí)和它的父類保持一致,一般情況下都是5,因?yàn)閙ain線程的優(yōu)先級(jí) 就是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());

上面程序的輸出結(jié)果是tl的優(yōu)先級(jí)為5,因?yàn)閙ain線程的優(yōu)先級(jí)是5 ; t2的優(yōu)先級(jí) 是6,因?yàn)轱@式地將其指定為6; t3的優(yōu)先級(jí)為6,沒有顯式地指定,因此其與父線程保持 一致。

獲取線程ID

public long getld()獲取線程的唯一 ID,線程的ID在整個(gè)JVM進(jìn)程中都會(huì)是唯一的,

并且是從0開始逐次遞增。如果你在main線程(main函數(shù))中創(chuàng)建了一個(gè)唯一的線程,并 且調(diào)用getld()后發(fā)現(xiàn)其并不等于0,也許你會(huì)納悶,不應(yīng)該是從0開始的嗎?之前已經(jīng)說 過了在一個(gè)JVM進(jìn)程啟動(dòng)的時(shí)候,實(shí)際上是開辟了很多個(gè)線程,自增序列已經(jīng)有了一定的 消耗,因此我們自己創(chuàng)建的線程絕非第0號(hào)線程。

獲取當(dāng)前線程

public static Thread currentThread()用于返回當(dāng)前執(zhí)行線程的引用,這個(gè)方法雖然很簡(jiǎn) 單,但是使用非常廣泛,我們?cè)诤竺娴膬?nèi)容中會(huì)大量的使用該方法,來看一段示例代碼:

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

上面程序運(yùn)行輸出的兩個(gè)結(jié)果都是true。

設(shè)置線程上下文類加載器

□ public ClassLoader getContextClassLoader()獲取線程上下文的類加載器,簡(jiǎn)單來說 就是這個(gè)線程是由哪個(gè)類加器加載的,如果是在沒有修改線程上下文類加載器的情 況下,則保持與父線程同樣的類加載器。

□ public void setContextClassLoader(ClassLoader cl)設(shè)置該線程的類加載器,這個(gè)方法 可以打破JAVA類加載器的父委托機(jī)制,有時(shí)候該方法也被稱為JAVA類加載器的 后門。

關(guān)于線程上下文類加載器的內(nèi)容我們將在本書的第11章重點(diǎn)介紹,并且結(jié)合jdbc驅(qū)動(dòng) 包的源碼分析JDK的開發(fā)者為什么要留有這樣的后門。

線程 interrupt

線程interrupt,是一個(gè)非常重要的API,也是經(jīng)常使用的方法,與線程中斷相關(guān)的API 有如下幾個(gè),在本節(jié)中我們也將Thread深入源碼對(duì)其進(jìn)行詳細(xì)的剖析。

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

interrupt

如下方法的調(diào)用會(huì)使得當(dāng)前線程進(jìn)入阻塞狀態(tài),而調(diào)用當(dāng)前線程的interrupt方法,就 可以打斷阻塞。

□ Object 的 wait 方法?!?nbsp;Object 的 wait(long)方法。□ Object 的 wait(long,int)方法。□ Thread 的 sleep(long)方法?!?nbsp;Thread 的 sleep(long,int)方法。□ Thread 的 join 方法。□ Thread 的 join(long)方法?!?nbsp;Thread 的 join(long,int)方法。□ InterruptibleChannel 的 io 操作。□ Selector 的 wakeup 方法。

□其他方法。

上述若干方法都會(huì)使得當(dāng)前線程進(jìn)入阻塞狀態(tài),若另外的一個(gè)線程調(diào)用被阻塞線程的 interrupt方法,則會(huì)打斷這種阻塞,因此這種方法有時(shí)會(huì)被稱為可中斷方法,.記住,打斷一 個(gè)線程并不等于該線程的生命周期結(jié)束,僅僅是打斷了當(dāng)前線程的阻塞狀態(tài)。

一旦線程在阻塞的情況下被打斷,都會(huì)拋出一個(gè)稱為InterruptedException的異常,這 個(gè)異常就像一個(gè)signal (信號(hào))一樣通知當(dāng)前線程被打斷了,下面我們來看一個(gè)例子:

package com.wangwenjun,concurrent,chapter03;import java.util.concurrent .TimeUnit;public class Threadinterruptpublic 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();}}

上面的代碼創(chuàng)建了一個(gè)線程,并且企圖休眠1分鐘的時(shí)長(zhǎng),不過很可惜,大約在2毫 秒之后就被主線程調(diào)用interrupt方法打斷,程序的執(zhí)行結(jié)果就是“Oh, i am be interrupted.”

interrupt這個(gè)方法到底做了什么樣的事情呢?在一個(gè)線程內(nèi)部存在著名為interrupt flag 的標(biāo)識(shí),如果一個(gè)線程被interrupt,那么它的flag將被設(shè)置,但是如果當(dāng)前線程正在執(zhí)行 可中斷方法被阻塞時(shí),調(diào)用interrupt方法將其中斷,反而會(huì)導(dǎo)致flag被清除,關(guān)于這點(diǎn)我 們?cè)诤竺孢€會(huì)做詳細(xì)的介紹。另外有一點(diǎn)需要注意的是,如果一個(gè)線程已經(jīng)是死亡狀態(tài), 那么嘗試對(duì)其的interrupt會(huì)直接被忽略。

islnterrupted

islnterrupted是Thread的一個(gè)成員方法,它主要判斷當(dāng)前線程是否被中斷,該方法僅 僅是對(duì)interrupt標(biāo)識(shí)的一個(gè)判斷,并不會(huì)影響標(biāo)識(shí)發(fā)生任何改變,這個(gè)與我們即將學(xué)習(xí)到 的interrupted是存在差別的,下面我們看一個(gè)簡(jiǎn)單的程序:

package com.wangwenjun,concurrent?chapter03;import java.util.concurrent.TimeUnit;public class ThreadisInterrupted{public static void main(String[] args) throws InterruptedExceptionThread thread = new Thread(){@Overridepublic 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());}}

上面的代碼中定義了一個(gè)線程,并且在線程的執(zhí)行單元中(run方法)寫了一個(gè)空的死 循環(huán),為什么不寫sleep呢?因?yàn)閟leep是可中斷方法,會(huì)捕獲到中斷信號(hào),從而干擾我們 程序的結(jié)果。下面是程序運(yùn)行的結(jié)果,記得手動(dòng)結(jié)束上面的程序運(yùn)行,或者你也可以將上 面定義的線程指定為守護(hù)線程,這樣就會(huì)隨著主線程的結(jié)束導(dǎo)致JVM中沒有非守護(hù)線程而 自動(dòng)退出。

Thread is interrupted ? falseThread is interrupted ? true

可中斷方法捕獲到了中斷信號(hào)(signal)之后,也就是捕獲了 InterruptedException異常 之后會(huì)擦除掉interrupt的標(biāo)識(shí),對(duì)上面的程序稍作修改,你會(huì)發(fā)現(xiàn)程序的結(jié)果又會(huì)出現(xiàn)很 大的不同,示例代碼如下:

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(){@Overridepublic 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這個(gè)可中斷方法,它會(huì)捕獲到中斷信號(hào),并且會(huì)擦除 interrupt標(biāo)識(shí),因此程序的執(zhí)行結(jié)果都會(huì)是false,程序輸岀如下:

Thread is interrupted ? falseI am be interrupted ? falseThread is interrupted ? false

其實(shí)這也不難理解,可中斷方法捕獲到了中斷信號(hào)之后,為了不影響線程中其他方法 的執(zhí)行,將線程的interrupt標(biāo)識(shí)復(fù)位是一種很合理的設(shè)計(jì)。

interrupted

interrupted是一個(gè)靜態(tài)方法,雖然其也用于判斷當(dāng)前線程是否被中斷,但是它和成員方 法islnterrupted還是有很大的區(qū)別的,調(diào)用該方法會(huì)直接擦除掉線程的interrupt標(biāo)識(shí),需 要注意的是,如果當(dāng)前線程被打斷了,那么第一次調(diào)用interrupted方法會(huì)返回true,并且 立即擦除了 interrupt標(biāo)識(shí);第二次包括以后的調(diào)用永遠(yuǎn)都會(huì)返回false,除非在此期間線程 又一次地被打斷,下面設(shè)計(jì)了一個(gè)簡(jiǎn)單的例子,來驗(yàn)證我們的說法:

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方法中沒有進(jìn)行任 何短暫的休眠,所以運(yùn)行上面的程序會(huì)出現(xiàn)非常多的輸出,但是我們通過對(duì)輸出的檢查會(huì) 發(fā)現(xiàn)如下所示的內(nèi)容,其足以作為對(duì)該方法的解釋。

false
false
true
false
false

在很多的false包圍中發(fā)現(xiàn)了一個(gè)true,也就是interrupted方法判斷到了其被中斷,立 即擦除了中斷標(biāo)識(shí),并且只有這一次返回true,后面的都將會(huì)是false。

interrupt 注意事項(xiàng)

打開Thread的源碼,不難發(fā)現(xiàn),islnterrupted方法和interrupted方法都調(diào)用了同一個(gè) 本地方法:

private native boolean islnterrupted(boolean Clearlnterrupted);

其中參數(shù)Clearlnterrupted主要用來控制是否擦除線程interrupt的標(biāo)識(shí)。

islnterrupted方法的源碼中該參數(shù)為false,表示不想擦除:

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

而interrupted靜態(tài)方法中該參數(shù)則為true,表示想要擦除:

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

在比較詳細(xì)地學(xué)習(xí)了 interrupt方法之后,大家思考一個(gè)問題,如果一個(gè)線程在沒有執(zhí) 行可中斷方法之前就被打斷,那么其接下來將執(zhí)行可中斷方法,比如sleep會(huì)發(fā)生什么樣的 情況呢?下面我們通過一個(gè)簡(jiǎn)單的實(shí)驗(yàn)來回答這個(gè)疑問:

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

通過運(yùn)行上面的程序,你會(huì)發(fā)現(xiàn),如果一個(gè)線程設(shè)置了 interrupt標(biāo)識(shí),那么接下來的 可中斷方法會(huì)立即中斷,因此注釋⑤的信號(hào)捕獲部分代碼會(huì)被執(zhí)行,請(qǐng)大家注意注釋①和注 釋③中判斷線程中斷方法的不同,也希望讀者結(jié)合本節(jié)的內(nèi)容思考為什么要這么做?

線程 join

Thread的join方法同樣是一個(gè)非常重要的方法,使用它的特性可以實(shí)現(xiàn)很多比較強(qiáng)大 的功能,與sleep -樣它也是一個(gè)可中斷的方法,也就是說,如果有其他線程執(zhí)行了對(duì)當(dāng)前 線程的interrupt操作,它也會(huì)捕獲到中斷信號(hào),并且擦除線程的interrupt標(biāo)識(shí),Thread的 API為我們提供了三個(gè)不同的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

在本節(jié)中,筆者將會(huì)詳細(xì)介紹join方法以及如何在實(shí)際應(yīng)用中使用join方法。

線程join方法詳解

join某個(gè)線程A,會(huì)使當(dāng)前線程B進(jìn)入等待,直到線程A結(jié)束生命周期,或者到達(dá)給 定的時(shí)間,那么在此期間B線程是處于BLOCKED的,而不是A線程,下面就來通過一個(gè) 簡(jiǎn)單的實(shí)例解釋一下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
{
//①定義兩個(gè)線程,并保存在threads中
List<Thread> threads = IntStream.range(1, 3)
.mapToObj(Threadjoin::create).collect(toList());
//②啟動(dòng)這兩個(gè)線程
threads?forEach(Thread::start);
//③ 執(zhí)行這兩個(gè)線程的join方法
for (Thread thread : threads)
{
thread.join();
}
//④main線程循環(huán)輸出
for (int i = 0; i < 10; i++)
System.out.printin(Thread.currentThread().getName() + "+ i); shortSleep();


//構(gòu)造一個(gè)簡(jiǎn)單的線程,每個(gè)線程只是簡(jiǎn)單的循環(huán)輸出
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();
}
}
}

上面的代碼結(jié)合Java 8的語(yǔ)法,創(chuàng)建了兩個(gè)線程,分別啟動(dòng),并且調(diào)用了每個(gè)線程的 join方法(注意:join方法是被主線程調(diào)用的,因此在第一個(gè)線程還沒有結(jié)束生命周期的時(shí) 后,第二個(gè)線程的join不會(huì)得到執(zhí)行,但是此時(shí),第二個(gè)線程也已經(jīng)啟動(dòng)了),運(yùn)行上面的 程序,你會(huì)發(fā)現(xiàn)線程一和線程二會(huì)交替地輸出直到它們結(jié)束生命周期,main線程的循環(huán)才 會(huì)開始運(yùn)行,程序輸岀如下:

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

如果你將注釋③下面的join全部注釋掉,那么三個(gè)線程將會(huì)交替地輸出,程序輸出如下:

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

join方法會(huì)使當(dāng)前線程永遠(yuǎn)地等待下去,直到期間被另外的線程中斷,或者join的線 程執(zhí)行結(jié)束,當(dāng)然你也可以使用join的另外兩個(gè)重載方法,指定毫秒數(shù),在指定的時(shí)間到 達(dá)之后,當(dāng)前線程也會(huì)退出阻塞。同樣思考一個(gè)問題,如果一個(gè)線程已經(jīng)結(jié)束了生命周期, 那么調(diào)用它的join方法的當(dāng)前線程會(huì)被阻塞嗎?

join方法結(jié)合實(shí)戰(zhàn)

本節(jié)我們將結(jié)合一個(gè)實(shí)際的案例,來看一下join方法的應(yīng)用場(chǎng)景,假設(shè)你有一個(gè)APP, 主要用于查詢航班信息,你的APP是沒有這些實(shí)時(shí)數(shù)據(jù)的,當(dāng)用戶發(fā)起查詢請(qǐng)求時(shí),你需 要到各大航空公司的接口獲取信息,最后統(tǒng)一整理加工返回到APP客戶端,如圖3-1所示, 當(dāng)然JDK自帶了很多高級(jí)工具,比如CountDownLatch和CyclicBarrier等都可以完成類似 的功能,但是僅就我們目前所學(xué)的知識(shí),使用join方法即可完成下面的功能。

什么是多線程源碼

該例子是典型的串行任務(wù)局部并行化處理,用戶在APP客戶端輸入出發(fā)地“北京”和 目的地“上?!?,服務(wù)器接收到這個(gè)請(qǐng)求之后,先來驗(yàn)證用戶的信息,然后到各大航空公司 的接口查詢信息,最后經(jīng)過整理加工返回給客戶端,每一個(gè)航空公司的接口不會(huì)都一樣, 獲取的數(shù)據(jù)格式也不一樣,查詢的速度也存在著差異,如果再跟航空公司進(jìn)行串行化交互 (逐個(gè)地查詢),很明顯客戶端需要等待很長(zhǎng)的時(shí)間,這樣的話,用戶體驗(yàn)就會(huì)非常差。如果 我們將每一個(gè)航空公司的查詢都交給一個(gè)線程去工作,然后在它們結(jié)束工作之后統(tǒng)一對(duì)數(shù) 據(jù)進(jìn)行整理,這樣就可以極大地節(jié)約時(shí)間,從而提高用戶體驗(yàn)效果。

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

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

在代碼清單3-1中,F(xiàn)ightQuery提供了一個(gè)返回方法,寫到這里大家應(yīng)該注意到了,不 管是Thread的run方法,還是Runnable接口,都是void返回類型,如果你想通過某個(gè)線 程的運(yùn)行得到結(jié)果,就需要自己定義一個(gè)返回的接口。

查詢Fight的task,其實(shí)就是一個(gè)線程的子類,主要用于到各大航空公司獲取數(shù)據(jù),示 例代碼如下:

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("[">接口定義好了,查詢航班數(shù)據(jù)的線程也有了,下面就來實(shí)現(xiàn)一下從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<>();
//②創(chuàng)建查詢航班信息的線程列表
List<FightQueryTask> tasks = fightCompany.stream()
.map(f -> createSearchTask(f, original, dest))
.collect(toList());
//③分別啟動(dòng)這幾個(gè)線程
tasks , forEach(Thread::start);

〃④分別調(diào)用每一個(gè)線程的join方法,阻塞當(dāng)前線程 tasks.forEach(t ->
try
t.join();
} catch (InterruptedException e)
{
}
});
//⑤在此之前,當(dāng)前線程會(huì)阻塞住,獲取每一個(gè)查詢線程的結(jié)果,并且加入到result中
tasks.stream().map(FightQuery::get).forEach(result::addAll);
return result;
}
FightQueryTask createSearchTask( fight,
original, String dest)
return new FightQueryTask(fight, original, dest);
} 上面的代碼,關(guān)鍵的地方已通過注釋解釋得非常清楚,主線程收到了 search請(qǐng)求之后, 交給了若干個(gè)查詢線程分別進(jìn)行工作,最后將每一個(gè)線程獲取的航班數(shù)據(jù)進(jìn)行統(tǒng)一的匯總。 由于每個(gè)航空公司的查詢時(shí)間可能不一樣,所以用了一個(gè)隨機(jī)值來反應(yīng)不同的查詢速度, 返回給客戶端(打印到控制臺(tái)),程序的執(zhí)行結(jié)果輸出如下:[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  如何關(guān)閉一?個(gè)線程JDK有一個(gè)Deprecated方法stop,但是該方法存在一個(gè)問題,JDK官方早已經(jīng)不推薦 使用,其在后面的版本中有可能會(huì)被移除,根據(jù)官網(wǎng)的描述,該方法在關(guān)閉線程時(shí)可能不 會(huì)釋放掉monitor的鎖,所以強(qiáng)烈建議不要使用該方法結(jié)束線程,本節(jié)將主要介紹幾種關(guān)閉 線程的方法。正常關(guān)閉\1. 線程結(jié)束生命周期正常結(jié)束線程運(yùn)行結(jié)束,完成了自己的使命之后,就會(huì)正常退出,如果線程中的任務(wù)耗時(shí)比較 短,或者時(shí)間可控,那么放任它正常結(jié)束就好了。\2. 捕獲中斷信號(hào)關(guān)閉線程我們通過new Thread的方式創(chuàng)建線程,這種方式看似很簡(jiǎn)單,其實(shí)它的派生成本是比 較高的,因此在一個(gè)線程中往往會(huì)循環(huán)地執(zhí)行某個(gè)任務(wù),比如心跳檢查,不斷地接收網(wǎng)絡(luò) 消息報(bào)文等,系統(tǒng)決定退出的時(shí)候,可以借助中斷線程的方式使其退出,示例代碼如下: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的標(biāo)識(shí)來決定是否退出的,如果在線程中執(zhí)行某 個(gè)可中斷方法,則可以通過捕獲中斷信號(hào)來決定是否退出。@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.");
} 上面的代碼執(zhí)行結(jié)果都會(huì)導(dǎo)致線程正常的結(jié)束,程序輸出如下:I will start work
System will be shutdown.
I will be exiting. \3. 使用volatile開關(guān)控制由于線程的interrupt標(biāo)識(shí)很有可能被擦除,或者邏輯單元中不會(huì)調(diào)用任何可中斷方法, 所以使用volatile修飾的開關(guān)flag關(guān)閉線程也是一種常用的做法,具體如下: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())
//正在運(yùn)行
}
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();
}
} 上面的例子中定義了一個(gè)closed開關(guān)變量,并且是使用volatile修飾(關(guān)于volatile關(guān) 鍵字會(huì)在本書的第3部分中進(jìn)行非常細(xì)致地講解,volatile關(guān)鍵字在Java中是一個(gè)革命性的 關(guān)鍵字,非常重要,它是Java原子變量以及并發(fā)包的基礎(chǔ))運(yùn)行上面的程序同樣也可以關(guān) 閉線程。異常退出在一個(gè)線程的執(zhí)行單元中,是不允許拋出checked異常的,不論Thread中的run方 法,還是Runnable中的run方法,如果線程在運(yùn)行過程中需要捕獲checked異常并且 判斷是否還有運(yùn)行下去的必要,那么此時(shí)可以將checked異常封裝成unchecked異常 (RuntimeException)拋出進(jìn)而結(jié)束線程的生命周期。

到此,相信大家對(duì)“什么是多線程源碼”有了更深的了解,不妨來實(shí)際操作一番吧!這里是億速云網(wǎng)站,更多相關(guān)內(nèi)容可以進(jìn)入相關(guān)頻道進(jìn)行查詢,關(guān)注我們,繼續(xù)學(xué)習(xí)!

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場(chǎng),如果涉及侵權(quán)請(qǐng)聯(lián)系站長(zhǎng)郵箱:is@yisu.com進(jìn)行舉報(bào),并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI