Java 多線程詳解

線程對象是可以產(chǎn)生線程的對象袱院。比如在 Java 平臺中 Thread 對象 董栽、Runnable 對象恼策。線程鸦致,是指正在執(zhí)行的一個指點令序列。在 Java 平臺上是指從一個線程對象的 start() 方法開始,運行 run() 方法體中的那一段相對獨立的過程分唾。相比于多進(jìn)程抗碰,多線程的優(yōu)勢有:

進(jìn)程之間不能共享數(shù)據(jù),線程可以绽乔;

系統(tǒng)創(chuàng)建進(jìn)程需要為該進(jìn)程重新分配系統(tǒng)資源改含,故創(chuàng)建線程代價比較小迄汛;

Java 語言內(nèi)置了多線程功能支持,簡化了 Java 多線程編程骤视。

創(chuàng)建線程和啟動

繼承 Thread 類創(chuàng)建線程類

通過繼承 Thread 類創(chuàng)建線程類的具體步驟和具體代碼如下:

定義一個繼承 Thread 類的子類鞍爱,并重寫該類的 run() 方法。

創(chuàng)建 Thread 子類的實例专酗,即創(chuàng)建了線程對象睹逃。

調(diào)用該線程對象的 start() 方法啟動線程。

classSomeTheadextendsThraad{

publicvoidrun(){

// do something here?

? ? }?

}

publicstaticvoidmain(String[] args){

SomeThread oneThread =newSomeThread();

// 步驟3:啟動線程:?

? ? oneThread.start();

}

實現(xiàn) Runnable 接口創(chuàng)建線程類

通過實現(xiàn) Runnable 接口創(chuàng)建線程類的具體步驟和具體代碼如下:

定義 Runnable 接口的實現(xiàn)類祷肯,并重寫該接口的 run() 方法沉填。

創(chuàng)建 Runnable 實現(xiàn)類的實例,并以此實例作為 Thread 的 target 對象佑笋,即該 Thread 對象才是真正的線程對象翼闹。

classSomeRunnableimplementsRunnable{

publicvoidrun(){

// do something here?

? ? }?

}

Runnable oneRunnable =newSomeRunnable();

Thread oneThread =newThread(oneRunnable);

oneThread.start();

通過 Callable 和 Future 創(chuàng)建線程

通過 Callable 和 Future 創(chuàng)建線程的具體步驟和具體代碼如下:

創(chuàng)建 Callable 接口的實現(xiàn)類,并實現(xiàn) call() 方法蒋纬,該 call() 方法將作為線程執(zhí)行體猎荠,并且有返回值。

創(chuàng)建 Callable 實現(xiàn)類的實例蜀备,使用 FutureTask 類來包裝 Callable 對象关摇,該 FutureTask 對象封裝了該 Callable 對象的 call() 方法的返回值。

使用 FutureTask 對象作為 Thread 對象的 target 創(chuàng)建并啟動新線程碾阁。

調(diào)用 FutureTask 對象的 get() 方法來獲得子線程執(zhí)行結(jié)束后的返回值其中输虱,Callable 接口(也只有一個方法)定義如下:

publicinterfaceCallable{

Vcall()throwsException;

}

// 步驟1:創(chuàng)建實現(xiàn) Callable 接口的類 SomeCallable (略);?

// 步驟2:創(chuàng)建一個類對象:

Callable oneCallable =newSomeCallable();

// 步驟3:由 Callable 創(chuàng)建一個 FutureTask 對象:?

FutureTask oneTask =newFutureTask(oneCallable);

// 注釋: FutureTask 是一個包裝器,它通過接受 Callable 來創(chuàng)建脂凶,它同時實現(xiàn)了 Future 和 Runnable 接口宪睹。

// 步驟4:由 FutureTask 創(chuàng)建一個 Thread 對象:?

Thread oneThread =newThread(oneTask);

// 步驟5:啟動線程:?

? ? oneThread.start();

線程的生命周期

新建狀態(tài)

用 new 關(guān)鍵字 和 Thread 類或其子類建立一個線程對象后,該線程對象就處于新生狀態(tài)艰猬。處于新生狀態(tài)的線程有自己的內(nèi)存空間横堡,通過調(diào)用 start() 方法進(jìn)入就緒狀態(tài)(runnable)。

注意:?不能對已經(jīng)啟動的線程再次調(diào)用 start() 方法冠桃,否則會出現(xiàn) Java.lang.IllegalThreadStateException 異常命贴。

就緒狀態(tài)

處于就緒狀態(tài)的線程已經(jīng)具備了運行條件,但還沒有分配到 CPU,處于線程就緒隊列(盡管是采用隊列形式胸蛛,事實上污茵,把它稱為可運行池而不是可運行隊列。因為 CPU 的調(diào)度不一定是按照先進(jìn)先出的順序來調(diào)度的)葬项,等待系統(tǒng)為其分配 CPU 泞当。等待狀態(tài)并不是執(zhí)行狀態(tài),當(dāng)系統(tǒng)選定一個等待執(zhí)行的 Thread 對象后民珍,它就會從等待執(zhí)行狀態(tài)進(jìn)入執(zhí)行狀態(tài)襟士,系統(tǒng)挑選的動作稱之為“CPU調(diào)度”。一旦獲得 CPU嚷量,線程就進(jìn)入運行狀態(tài)并自動調(diào)用自己的 run() 方法陋桂。

提示:如果希望子線程調(diào)用 start() 方法后立即執(zhí)行,可以使用 Thread.sleep() 方式使主線程睡眠一會兒蝶溶,轉(zhuǎn)去執(zhí)行子線程嗜历。

運行狀態(tài)

處于運行狀態(tài)的線程最為復(fù)雜,它可以變?yōu)樽枞麪顟B(tài)抖所、就緒狀態(tài)和死亡狀態(tài)梨州。

處于就緒狀態(tài)的線程,如果獲得了 CPU 的調(diào)度田轧,就會從就緒狀態(tài)變?yōu)檫\行狀態(tài)暴匠,執(zhí)行 run() 方法中的任務(wù)。如果該線程失去了 CPU 資源涯鲁,就會又從運行狀態(tài)變?yōu)榫途w狀態(tài)巷查。重新等待系統(tǒng)分配資源。也可以對在運行狀態(tài)的線程調(diào)用 yield() 方法抹腿,它就會讓出 CPU 資源岛请,再次變?yōu)榫途w狀態(tài)。

當(dāng)發(fā)生如下情況是警绩,線程會從運行狀態(tài)變?yōu)樽枞麪顟B(tài):

線程調(diào)用 sleep() 方法主動放棄所占用的系統(tǒng)資源崇败。

線程調(diào)用一個阻塞式 IO 方法,在該方法返回之前肩祥,該線程被阻塞后室。

線程試圖獲得一個同步監(jiān)視器,但更改同步監(jiān)視器正被其他線程所持有混狠。

線程在等待某個通知(notify)岸霹。

程序調(diào)用了線程的 suspend() 方法將線程掛起。不過該方法容易導(dǎo)致死鎖将饺,所以程序應(yīng)該盡量避免使用該方法贡避。

當(dāng)線程的 run() 方法執(zhí)行完痛黎,或者被強(qiáng)制性地終止,例如出現(xiàn)異常刮吧,或者調(diào)用了 stop() 湖饱、desyory() 方法等等,就會從運行狀態(tài)轉(zhuǎn)變?yōu)樗劳鰻顟B(tài)杀捻。

阻塞狀態(tài)

處于運行狀態(tài)的線程在某些情況下井厌,如執(zhí)行了 sleep() 方法,或等待 I/O 設(shè)備等資源致讥,將讓出 CPU 并暫時停止自己的運行仅仆,進(jìn)入阻塞狀態(tài)。

在阻塞狀態(tài)的線程不能進(jìn)入就緒隊列垢袱。只有當(dāng)引起阻塞的原因消除時蝇恶,如睡眠時間已到,或等待的 I/O 設(shè)備空閑下來惶桐,線程便轉(zhuǎn)入就緒狀態(tài),重新到就緒隊列中排隊等待潘懊,被系統(tǒng)選中后從原來停止的位置開始繼續(xù)運行姚糊。可以分為三種:

等待阻塞:運行狀態(tài)中的線程執(zhí)行 wait() 方法授舟,使線程進(jìn)入到等待阻塞狀態(tài)救恨。

同步阻塞:線程在獲取 synchronized 同步鎖失敗(因為同步鎖被其他線程占用)释树。

其他阻塞:通過調(diào)用線程的 sleep() 或 join() 發(fā)出了 I/O 請求時肠槽,線程就會進(jìn)入到阻塞狀態(tài)。當(dāng) sleep() 狀態(tài)超時奢啥,join() 等待線程終止或超時秸仙,或者 I/O 處理完畢,線程重新轉(zhuǎn)入就緒狀態(tài)桩盲。

死亡狀態(tài)

當(dāng)線程的 run() 方法執(zhí)行完寂纪,或者被強(qiáng)制性地終止,就認(rèn)為它死去赌结。這個線程對象也許是活的捞蛋,但是,它已經(jīng)不是一個單獨執(zhí)行的線程柬姚。線程一旦死亡拟杉,就不能復(fù)生。 如果在一個死去的線程上調(diào)用 start() 方法量承,會拋出 java.lang.IllegalThreadStateException 異常搬设。

線程管理

Java 提供了一些便捷的方法用于會線程狀態(tài)的控制穴店。具體如下:

線程睡眠 —— sleep

如果我們需要讓當(dāng)前正在執(zhí)行的線程暫停一段時間,并進(jìn)入阻塞狀態(tài)焕梅,則可以通過調(diào)用 Thread 的 sleep() 方法迹鹅。

注意:

sleep() 是靜態(tài)方法,最好不要用 Thread 的實例對象調(diào)用它贞言,因為它睡眠的始終是當(dāng)前正在運行的線程斜棚,而不是調(diào)用它的線程對象,它只對正在運行狀態(tài)的線程對象有效该窗。如下面的例子:

publicclassTest1{

publicstaticvoidmain(String[] args)throwsInterruptedException{

? ? System.out.println(Thread.currentThread().getName());?

MyThread myThread=newMyThread();

? ? myThread.start();?

myThread.sleep(1000);//這里sleep的就是main線程弟蚀,而非myThread線程?

Thread.sleep(10);

for(inti=0;i<100;i++) {

System.out.println("main"+i);

? ? }?

}?

}

Java 線程調(diào)度是 Java 多線程的核心,只有良好的調(diào)度酗失,才能充分發(fā)揮系統(tǒng)的性能义钉,提高程序的執(zhí)行效率。但是不管程序員怎么編寫調(diào)度规肴,只能最大限度的影響線程執(zhí)行的次序捶闸,而不能做到精準(zhǔn)控制。因為使用 sleep() 方法之后拖刃,線程是進(jìn)入阻塞狀態(tài)的删壮,只有當(dāng)睡眠的時間結(jié)束,才會重新進(jìn)入到就緒狀態(tài)兑牡,而就緒狀態(tài)進(jìn)入到運行狀態(tài)央碟,是由系統(tǒng)控制的,我們不可能精準(zhǔn)的去干涉它均函,所以如果調(diào)用 Thread.sleep(1000) 使得線程睡眠 1 秒亿虽,可能結(jié)果會大于 1 秒。

線程讓步 —— yield

yield() 方法 和 sleep() 方法有點相似苞也,它也是 Thread 類提供的一個靜態(tài)的方法洛勉,它也可以讓當(dāng)前正在執(zhí)行的線程暫停,讓出 CPU 資源給其他的線程如迟。但是和 sleep() 方法不同的是坯认,它不會進(jìn)入到阻塞狀態(tài),而是進(jìn)入到就緒狀態(tài)氓涣。yield() 方法只是讓當(dāng)前線程暫停一下牛哺,重新進(jìn)入就緒的線程池中,讓系統(tǒng)的線程調(diào)度器重新調(diào)度器重新調(diào)度一次劳吠,完全可能出現(xiàn)這樣的情況:當(dāng)某個線程調(diào)用 yield() 方法之后引润,線程調(diào)度器又將其調(diào)度出來重新進(jìn)入到運行狀態(tài)執(zhí)行。

實際上痒玩,當(dāng)某個線程調(diào)用了 yield() 方法暫停之后淳附,優(yōu)先級與當(dāng)前線程相同议慰,或者優(yōu)先級比當(dāng)前線程更高的就緒狀態(tài)的線程更有可能獲得執(zhí)行的機(jī)會,當(dāng)然奴曙,只是有可能别凹,因為我們不可能精確的干涉 CPU 調(diào)度線程。用法如下:

publicclassTest1{

publicstaticvoidmain(String[] args)throwsInterruptedException{

newMyThread("低級",1).start();

newMyThread("中級",5).start();

newMyThread("高級",10).start();

? ? }?

}?

classMyThreadextendsThread{

publicMyThread(String name,intpro){

super(name);// 設(shè)置線程的名稱?

this.setPriority(pro);// 設(shè)置優(yōu)先級?

? ? }?

@Override

publicvoidrun(){

for(inti =0; i <30; i++) {

System.out.println(this.getName() +"線程第"+ i +"次執(zhí)行洽糟!");

if(i %5==0) {

? ? ? ? ? ? ? ? Thread.yield();

? ? ? ? ? ? }?

? ? ? ? }?

? ? }?

}

關(guān)于 sleep() 方法和 yield() 方的區(qū)別如下:

sleep() 方法暫停當(dāng)前線程后炉菲,會進(jìn)入阻塞狀態(tài),只有當(dāng)睡眠時間到了坤溃,才會轉(zhuǎn)入就緒狀態(tài)拍霜。而 yield() 方法調(diào)用后 ,是直接進(jìn)入就緒狀態(tài)薪介,所以有可能剛進(jìn)入就緒狀態(tài)祠饺,又被調(diào)度到運行狀態(tài)。

sleep() 方法聲明拋出了 InterruptedException 汁政,所以調(diào)用 sleep() 方法的時候要捕獲該異常道偷,或者顯示聲明拋出該異常。而 yield() 方法則沒有聲明拋出任務(wù)異常记劈。

sleep() 方法比 yield() 方法有更好的可移植性试疙,通常不要依靠 yield() 方法來控制并發(fā)線程的執(zhí)行。

線程合并 —— join

線程的合并的含義就是將幾個并行線程的線程合并為一個單線程執(zhí)行抠蚣,應(yīng)用場景是當(dāng)一個線程必須等待另一個線程執(zhí)行完畢才能執(zhí)行時,Thread 類提供了 join() 方法來完成這個功能履澳,注意嘶窄,它不是靜態(tài)方法。

它有3個重載的方法:

// 當(dāng)前線程等該加入該線程后面距贷,等待該線程終止。

voidjoin()

// 當(dāng)前線程等待該線程終止的時間最長為 millis 毫秒。 如果在 millis 時間內(nèi)巡社,該線程沒有執(zhí)行完椿访,那么當(dāng)前線程進(jìn)入就緒狀態(tài),重新等待 CPU 調(diào)度?

voidjoin(longmillis)

// 等待該線程終止的時間最長為 millis 毫秒 + nanos 納秒阁最。如果在 millis 時間內(nèi)戒祠,該線程沒有執(zhí)行完,那么當(dāng)前線程進(jìn)入就緒狀態(tài)速种,重新等待 CPU 調(diào)度?

voidjoin(longmillis,intnanos)

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

每個線程執(zhí)行時都有一個優(yōu)先級的屬性姜盈,優(yōu)先級高的線程可以獲得較多的執(zhí)行機(jī)會,而優(yōu)先級低的線程則獲得較少的執(zhí)行機(jī)會配阵。與線程休眠類似馏颂,線程的優(yōu)先級仍然無法保障線程的執(zhí)行次序示血。只不過,優(yōu)先級高的線程獲取 CPU 資源的概率較大救拉,優(yōu)先級低的也并非沒機(jī)會執(zhí)行难审。

每個線程默認(rèn)的優(yōu)先級都與創(chuàng)建它的父線程具有相同的優(yōu)先級,在默認(rèn)情況下亿絮,main 線程具有普通優(yōu)先級告喊。

Thread 類提供了 setPriority(int newPriority) 和 getPriority() 方法來設(shè)置和返回一個指定線程的優(yōu)先級,其中 setPriority(int newPriority) 方法的參數(shù)是一個整數(shù)壹无,范圍是 1~·0 之間葱绒,也可以使用 Thread 類提供的三個靜態(tài)常量:

MAX_PRIORITY? =10

MIN_PRIORITY? =1

NORM_PRIORITY? =5

public class Test1 {?

? ? public static void main(String[] args) throws InterruptedException {?

? ? ? ? new MyThread("高級", 10).start();?

? ? ? ? new MyThread("低級", 1).start();?

? ? }?

}?

class MyThread extends Thread {?

? ? public MyThread(String name,int pro) {?

? ? ? ? super(name); // 設(shè)置線程的名稱?

? ? ? ? setPriority(pro); // 設(shè)置線程的優(yōu)先級?

? ? }?

? ? @Override?

? ? public void run() {?

? ? ? ? for (int i = 0; i < 100; i++) {?

? ? ? ? ? ? System.out.println(this.getName() + "線程第" + i + "次執(zhí)行!");?

? ? ? ? }?

? ? }?

}

注意:?雖然 Java 提供了 10 個優(yōu)先級別斗锭,但這些優(yōu)先級別需要操作系統(tǒng)的支持地淀。不同的操作系統(tǒng)的優(yōu)先級并不相同,而且也不能很好的和 Java 的 10 個優(yōu)先級別對應(yīng)岖是。所以我們應(yīng)該使用 MAX_PRIORITY 帮毁、 MIN_PRIORITY 和 NORM_PRIORITY 三個靜態(tài)常量來設(shè)定優(yōu)先級,這樣才能保證程序最好的可移植性豺撑。

后臺(守護(hù))線程

守護(hù)線程使用的情況較少烈疚,但并非無用,舉例來說聪轿,JVM 的垃圾回收爷肝、內(nèi)存管理等線程都是守護(hù)線程。還有就是在做數(shù)據(jù)庫應(yīng)用時候陆错,使用的數(shù)據(jù)庫連接池灯抛,連接池本身也包含著很多后臺線程,監(jiān)控連接個數(shù)音瓷、超時時間对嚼、狀態(tài)等等。調(diào)用線程對象的方法 setDaemon(true) 绳慎,則可以將其設(shè)置為守護(hù)線程纵竖。守護(hù)線程的用途為:

守護(hù)線程通常用于執(zhí)行一些后臺作業(yè),例如在你的應(yīng)用程序運行時播放背景音樂杏愤,在文字編輯器里做自動語法檢查靡砌、自動保存等功能。

Java 的垃圾回收也是一個守護(hù)線程珊楼。守護(hù)線的好處就是你不需要關(guān)心它的結(jié)束問題乏奥。例如你在你的應(yīng)用程序運行的時候希望播放背景音樂,如果將這個播放背景音樂的線程設(shè)定為非守護(hù)線程亥曹,那么在用戶請求退出的時候邓了,不僅要退出主線程恨诱,還要通知播放背景音樂的線程退出;如果設(shè)定為守護(hù)線程則不需要了骗炉。

setDaemon 方法的詳細(xì)說明:

// 將該線程標(biāo)記為守護(hù)線程或用戶線程照宝。當(dāng)正在運行的線程都是守護(hù)線程時,Java 虛擬機(jī)退出句葵。

// 該方法必須在啟動線程前調(diào)用厕鹃。 該方法首先調(diào)用該線程的 checkAccess 方法,且不帶任何參數(shù)乍丈。這可能拋出 SecurityException(在當(dāng)前線程中)剂碴。

// 參數(shù):on 如果為 true,則將該線程標(biāo)記為守護(hù)線程轻专。

// 如果該線程處于活動狀態(tài)忆矛,拋出 IllegalThreadStateException異常。 如果當(dāng)前線程無法修改該線程请垛,拋出 SecurityException 異常催训。

publicfinalvoidsetDaemon(booleanon)

注意:?JRE 判斷程序是否執(zhí)行結(jié)束的標(biāo)準(zhǔn)是所有的前臺執(zhí)線程行完畢了,而不管后臺線程的狀態(tài)宗收,因此漫拭,在使用后臺線程的時候一定要注意這個問題。

正確結(jié)束線程

Thread.stop() 混稽、Thread.suspend 采驻、Thread.resume 、Runtime.runFinalizersOnExit 這些終止線程運行的方法已經(jīng)被廢棄了匈勋,使用它們是極端不安全的礼旅,想要安全有效的結(jié)束一個線程,可以使用下面的方法:

正常執(zhí)行完 run() 方法颓影,然后結(jié)束掉。

控制循環(huán)條件和判斷條件的標(biāo)識符來結(jié)束掉線程懒鉴。

classMyThreadextendsThread{

inti=0;

booleannext=true;

@Override

publicvoidrun(){

while(next) {

if(i==10) {

next=false;

? ? ? ? ? ? }?

? ? ? ? ? ? i++;?

? ? ? ? ? ? System.out.println(i);?

? ? ? ? }?

? ? }?

}

線程同步

Java 允許多線程并發(fā)控制诡挂,當(dāng)多個線程同時操作一個可共享的資源變量時(如:數(shù)據(jù)的增刪改查),將會導(dǎo)致數(shù)據(jù)不準(zhǔn)確临谱,相互之間產(chǎn)生沖突璃俗,因此加入同步鎖以避免在該線程沒有完成操作之前,被其他線程的調(diào)用悉默,從而保證了該變量的唯一性和準(zhǔn)確性城豁。

同步方法

即用 synchronized 關(guān)鍵字修飾的方法。由于 Java 的每個對象都有一個內(nèi)置鎖抄课,當(dāng)用此關(guān)鍵字修飾方法時唱星,內(nèi)置鎖會保護(hù)整個方法雳旅。在調(diào)用該方法前,需要獲得內(nèi)置鎖间聊,否則就處于阻塞狀態(tài)攒盈。

publicsynchronizedvoidsave(){}

注意:?synchronized 關(guān)鍵字也可以修飾靜態(tài)方法,此時如果調(diào)用該靜態(tài)方法哎榴,將會鎖住整個類型豁。

同步代碼塊

即用 synchronized 關(guān)鍵字修飾的語句塊。被該關(guān)鍵字修飾的語句塊會自動被加上內(nèi)置鎖尚蝌,從而實現(xiàn)同步迎变。

publicclassBank{

privateintcount =0;// 賬戶余額?

// 存錢?

publicvoidaddMoney(intmoney){

synchronized(this) {

? ? ? ? ? ? count +=money;?

? ? ? ? }

System.out.println(System.currentTimeMillis()+"存進(jìn):"+money);

? ? }?

// 取錢?

publicvoidsubMoney(intmoney){

synchronized(this) {

if(count-money <0) {

System.out.println("余額不足");

return;

? ? ? ? ? ? }?

? ? ? ? ? ? count -=money;?

? ? ? ? }?

System.out.println(+System.currentTimeMillis()+"取出:"+money);

? ? }?

// 查詢?

publicvoidlookMoney(){

System.out.println("賬戶余額:"+count);

? ? }

}

注意:?同步是一種高開銷的操作,因此應(yīng)該盡量減少同步的內(nèi)容飘言。通常沒有必要同步整個方法衣形,使用 synchronized 代碼塊同步關(guān)鍵代碼即可。

使用特殊域變量(volatile)實現(xiàn)線程同步

volatile關(guān)鍵字為域變量的訪問提供了一種免鎖機(jī)制热凹。

使用volatile修飾域相當(dāng)于告訴虛擬機(jī)該域可能會被其他線程更新泵喘。

因此每次使用該域就要重新計算,而不是使用寄存器中的值般妙。

volatile不會提供任何原子操作纪铺,它也不能用來修飾 final 類型的變量。

publicclassSynchronizedThread{

classBank{

privatevolatileintaccount =100;

publicintgetAccount(){

returnaccount;

? ? ? ? }

/**

? ? ? ? * 用同步方法實現(xiàn)

? ? ? ? *

*@parammoney

? ? ? ? */

publicsynchronizedvoidsave(intmoney){

? ? ? ? ? ? account += money;

? ? ? ? }

/**

? ? ? ? * 用同步代碼塊實現(xiàn)

? ? ? ? *

*@parammoney

? ? ? ? */

publicvoidsave1(intmoney){

synchronized(this) {

? ? ? ? ? ? ? ? account += money;

? ? ? ? ? ? }

? ? ? ? }

? ? }

classNewThreadimplementsRunnable{

privateBank bank;

publicNewThread(Bank bank){

this.bank = bank;

? ? ? ? }

@Override

publicvoidrun(){

for(inti =0; i <10; i++) {

// bank.save1(10);

bank.save(10);

System.out.println(i +"賬戶余額為:"+bank.getAccount());

? ? ? ? ? ? }

? ? ? ? }

? ? }

/**

? ? * 建立線程碟渺,調(diào)用內(nèi)部類

? ? */

publicvoiduseThread(){

Bank bank =newBank();

NewThread new_thread =newNewThread(bank);

System.out.println("線程1");

Thread thread1 =newThread(new_thread);

? ? ? ? thread1.start();

System.out.println("線程2");

Thread thread2 =newThread(new_thread);

? ? ? ? thread2.start();

? ? }

publicstaticvoidmain(String[] args){

SynchronizedThread st =newSynchronizedThread();

? ? ? ? st.useThread();

? ? }

}

注意:?多線程中的非同步問題主要出現(xiàn)在對域的讀寫上鲜锚,如果讓域自身避免這個問題,則就不需要修改操作該域的方法苫拍。用 final 域芜繁,有鎖保護(hù)的域 和 volatile 域可以避免非同步的問題。

使用重入鎖(Lock)實現(xiàn)線程同步

在 JavaSE 5.0 中新增了一個 java.util.concurrent 包來支持同步绒极。ReentrantLock 類是可重入骏令、互斥、實現(xiàn)了 Lock 接口的鎖垄提,它與使用 synchronized 方法和塊榔袋,具有相同的基本行為和語義,并且擴(kuò)展了其能力铡俐。ReenreantLock 類的常用方法有:

ReentrantLock() : 創(chuàng)建一個 ReentrantLock 實例? ? ? ?

lock() : 獲得鎖? ? ? ?

unlock() : 釋放鎖

// 只給出要修改的代碼凰兑,其余代碼與上同

classBank{

privateintaccount =100;

// 需要聲明這個鎖

privateLock lock =newReentrantLock();

publicintgetAccount(){

returnaccount;

? ? }

// 這里不再需要synchronized

publicvoidsave(intmoney){

? ? ? ? lock.lock();

try{

? ? ? ? ? ? account += money;

}finally{

? ? ? ? ? ? lock.unlock();

? ? ? ? }

? ? }

注意:?ReentrantLock 類還有一個可以創(chuàng)建公平鎖的構(gòu)造方法,但由于能大幅度降低程序運行效率审丘,不推薦使用吏够。

線程通信

借助于 Object 類的 wait() 、notify() 和 notifyAll() 實現(xiàn)通信

線程執(zhí)行 wait() 后,就放棄了運行資格锅知,處于凍結(jié)狀態(tài)播急;

線程運行時,內(nèi)存中會建立一個線程池喉镰,凍結(jié)狀態(tài)的線程都存在于線程池中旅择,notify() 執(zhí)行時喚醒的也是線程池中的線程,線程池中有多個線程時喚醒第一個被凍結(jié)的線程侣姆。

notifyall() 生真,喚醒線程池中所有線程。

注意:

wait() 捺宗、notify() 柱蟀、 notifyall() 都用在同步里面,因為這 3 個函數(shù)是對持有鎖的線程進(jìn)行操作蚜厉,而只有同步才有鎖长已,所以要使用在同步中。

wait() 昼牛、 notify() 术瓮、 notifyall() 在使用時必須標(biāo)識它們所操作的線程持有的鎖,因為等待和喚醒必須是同一鎖下的線程贰健,而鎖可以是任意對象胞四,所以這 3 個方法都是 Object 類中的方法。

單個消費者生產(chǎn)者例子如下:

// 生產(chǎn)者和消費者都要操作的資源

classResource{

privateString name;

privateintcount=1;

privatebooleanflag=false;

publicsynchronizedvoidset(String name){

if(flag) {

try{

? ? ? ? ? ? ? ? wait();

}catch(Exception e) {}

? ? ? ? }

this.name=name+"---"+count++;

System.out.println(Thread.currentThread().getName()+"...生產(chǎn)者..."+this.name);

flag=true;

this.notify();

? ? }?

publicsynchronizedvoidout(){

if(!flag) {

try{

? ? ? ? ? ? ? ? wait();

}catch(Exception e) {}

? ? ? ? }

System.out.println(Thread.currentThread().getName()+"...消費者..."+this.name);

flag=false;

this.notify();

? ? }?

}

classProducerimplementsRunnable{

privateResource res;

? ? Producer(Resource res) {?

this.res=res;

? ? }?

publicvoidrun(){

while(true) {

res.set("商品");

? ? ? ? }?

? ? }?

}?

classConsumerimplementsRunnable{

privateResource res;

? ? Consumer(Resource res) {?

this.res=res;

? ? }?

publicvoidrun(){

while(true) {

? ? ? ? ? ? res.out();?

? ? ? ? }?

? ? }?

}?

publicclassProducerConsumerDemo{

publicstaticvoidmain(String[] args){

Resource r=newResource();

Producer pro=newProducer(r);

Consumer con=newConsumer(r);

Thread t1=newThread(pro);

Thread t2=newThread(con);

? ? ? ? t1.start();?

? ? ? ? t2.start();?

? ? }

}

// 運行結(jié)果正常伶椿,生產(chǎn)者生產(chǎn)一個商品辜伟,緊接著消費者消費一個商品。

但是如果有多個生產(chǎn)者和多個消費者脊另,上面的代碼是有問題导狡,比如 2 個生產(chǎn)者,2 個消費者偎痛,運行結(jié)果就可能出現(xiàn)生產(chǎn)的 1 個商品生產(chǎn)了一次而被消費了 2 次旱捧,或者連續(xù)生產(chǎn) 2 個商品而只有 1 個被消費,這是因為此時共有 4 個線程在操作 Resource 對象踩麦,而 notify() 喚醒的是線程池中第 1 個 wait() 的線程枚赡,所以生產(chǎn)者執(zhí)行 notify() 時,喚醒的線程有可能是另 1 個生產(chǎn)者線程靖榕,這個生產(chǎn)者線程從 wait() 中醒來后不會再判斷 flag 标锄,而是直接向下運行打印出一個新的商品顽铸,這樣就出現(xiàn)了連續(xù)生產(chǎn) 2 個商品茁计。

為了避免這種情況,修改代碼如下:

classResource{

privateString name;

privateintcount=1;

privatebooleanflag=false;

publicsynchronizedvoidset(String name){

// 原先是 if,現(xiàn)在改成 while星压,這樣生產(chǎn)者線程從凍結(jié)狀態(tài)醒來時践剂,還會再判斷 flag

while(flag) {

try{

? ? ? ? ? ? ? ? wait();

}catch(Exception e) {}

? ? ? ? }

this.name=name+"---"+count++;

System.out.println(Thread.currentThread().getName()+"...生產(chǎn)者..."+this.name);

flag=true;

// 原先是 notity(),現(xiàn)在改成 notifyAll()娜膘,這樣生產(chǎn)者線程生產(chǎn)完一個商品后可以將等待中的消費者線程喚醒逊脯,否則只將上面改成 while 后,可能出現(xiàn)所有生產(chǎn)者和消費者都在 wait() 的情況竣贪。

this.notifyAll();

? ? }?

publicsynchronizedvoidout(){

// 原先是 if军洼,現(xiàn)在改成while,這樣消費者線程從凍結(jié)狀態(tài)醒來時演怎,還會再判斷flag

while(!flag) {

try{

? ? ? ? ? ? ? ? wait();

}catch(Exception e) {}

? ? ? ? }

System.out.println(Thread.currentThread().getName()+"...消費者..."+this.name);

flag=false;

// 原先是 notity()匕争,現(xiàn)在改成 notifyAll(),這樣消費者線程消費完一個商品后可以將等待中的生產(chǎn)者線程喚醒爷耀,否則只將上面改成 while 后甘桑,可能出現(xiàn)所有生產(chǎn)者和消費者都在 wait() 的情況。

this.notifyAll();

? ? }?

}?

publicclassProducerConsumerDemo{

publicstaticvoidmain(String[] args){

Resource r=newResource();

Producer pro=newProducer(r);

Consumer con=newConsumer(r);

Thread t1=newThread(pro);

Thread t2=newThread(con);

Thread t3=newThread(pro);

Thread t4=newThread(con);

? ? ? ? t1.start();?

? ? ? ? t2.start();?

? ? ? ? t3.start();?

? ? ? ? t4.start();?

? ? }?

}

使用 Condition 控制線程通信

jdk 1.5 中歹叮,提供了多線程的升級解決方案為:

將同步 synchronized 替換為顯式的 Lock 操作跑杭。

將 Object 類中的 wait()、notify()咆耿、notifyAll() 替換成了 Condition 對象德谅,該對象可以通過 Lock 鎖對象獲取。

一個 Lock 對象上可以綁定多個 Condition 對象票灰,這樣實現(xiàn)了本方線程只喚醒對方線程女阀,而 jdk 1.5 之前,一個同步只能有一個鎖屑迂,不同的同步只能用鎖來區(qū)分浸策,且鎖嵌套時容易死鎖。

classResource{

privateString name;

privateintcount=1;

privatebooleanflag=false;

privateLock lock =newReentrantLock();// Lock 是一個接口惹盼,ReentrantLock 是該接口的一個直接子類

privateCondition condition_pro=lock.newCondition();// 創(chuàng)建代表生產(chǎn)者方面的 Condition對象?

privateCondition condition_con=lock.newCondition();// 使用同一個鎖庸汗,創(chuàng)建代表消費者方面的 Condition 對象

publicvoidset(String name){

lock.lock();// 鎖住此語句與 lock.unlock() 之間的代碼?

try{

while(flag) {

condition_pro.await();// 生產(chǎn)者線程在 conndition_pro 對象上等待?

? ? ? ? ? ? }

this.name=name+"---"+count++;

System.out.println(Thread.currentThread().getName()+"...生產(chǎn)者..."+this.name);

flag=true;

? ? ? ? ? ? condition_con.signalAll();?

? ? ? ? }?

finally{

lock.unlock();// unlock() 要放在 finally 塊中

? ? ? ? }?

? ? }?

publicvoidout(){

lock.lock();// 鎖住此語句與 lock.unlock() 之間的代碼?

try{

while(!flag) {

condition_con.await();// 消費者線程在 conndition_con 對象上等待

? ? ? ? ? ? }

System.out.println(Thread.currentThread().getName()+"...消費者..."+this.name);

flag=false;

condition_pro.signqlAll();// 喚醒所有在 condition_pro 對象下等待的線程,也就是喚醒所有生產(chǎn)者線程?

? ? ? ? }?

finally{

? ? ? ? ? ? lock.unlock();?

? ? ? ? }?

? ? }?

}

使用阻塞隊列(BlockingQueue)控制線程通信

BlockingQueue 是一個接口手报,也是 Queue 的子接口蚯舱。BlockingQueue 具有一個特征:當(dāng)生產(chǎn)者線程試圖向 BlockingQueue 中放入元素時,如果該隊列已滿掩蛤,則線程被阻塞枉昏;但消費者線程試圖從 BlockingQueue 中取出元素時,如果隊列已空揍鸟,則該線程阻塞兄裂。程序的兩個線程通過交替向 BlockingQueue 中放入元素、取出元素,即可很好地控制線程的通信晰奖。

BlockingQueue 提供如下兩個支持阻塞的方法:

put(E e):嘗試把 Eu 元素放如 BlockingQueue 中谈撒,如果該隊列的元素已滿,則阻塞該線程匾南。

take():嘗試從 BlockingQueue 的頭部取出元素啃匿,如果該隊列的元素已空,則阻塞該線程蛆楞。

BlockingQueue 繼承了 Queue 接口溯乒,當(dāng)然也可以使用 Queue 接口中的方法,這些方法歸納起來可以分為如下三組:

在隊列尾部插入元素豹爹,包括 add(E e)橙数、offer(E e)、put(E e)方法帅戒,當(dāng)該隊列已滿時灯帮,這三個方法分別會拋出異常、返回 false逻住、阻塞隊列钟哥。

在隊列頭部刪除并返回刪除的元素。包括 remove()瞎访、poll()腻贰、和 take()方法,當(dāng)該隊列已空時扒秸,這三個方法分別會拋出異常播演、返回 false、阻塞隊列伴奥。

在隊列頭部取出但不刪除元素写烤。包括 element()和 peek()方法,當(dāng)隊列已空時拾徙,這兩個方法分別拋出異常洲炊、返回 false。

BlockingQueue 接口包含如下 5 個實現(xiàn)類:

ArrayBlockingQueue :基于數(shù)組實現(xiàn)的 BlockingQueue 隊列尼啡。

LinkedBlockingQueue:基于鏈表實現(xiàn)的 BlockingQueue 隊列暂衡。

PriorityBlockingQueue:它并不是保準(zhǔn)的阻塞隊列,該隊列調(diào)用 remove()崖瞭、poll()狂巢、take()等方法提取出元素時,并不是取出隊列中存在時間最長的元素书聚,而是隊列中最小的元素唧领。它判斷元素的大小即可根據(jù)元素(實現(xiàn) Comparable 接口)的本身大小來自然排序代态,也可使用 Comparator 進(jìn)行定制排序。

SynchronousQueue:同步隊列疹吃。對該隊列的存、取操作必須交替進(jìn)行西雀。

DelayQueue:它是一個特殊的 BlockingQueue萨驶,底層基于 PriorityBlockingQueue 實現(xiàn),不過艇肴,DelayQueue 要求集合元素都實現(xiàn) Delay 接口(該接口里只有一個longgetDelay()方法)腔呜,DelayQueue 根據(jù)集合元素的 getDalay()方法的返回值進(jìn)行排序。

示例:

importjava.util.concurrent.ArrayBlockingQueue;

importjava.util.concurrent.BlockingQueue;

publicclassBlockingQueueTest{

publicstaticvoidmain(String[] args)throwsException{

// 創(chuàng)建一個容量為 1 的 BlockingQueue

BlockingQueue b=newArrayBlockingQueue<>(1);

// 啟動 3 個生產(chǎn)者線程

newProducer(b).start();

newProducer(b).start();

newProducer(b).start();

// 啟動一個消費者線程

newConsumer(b).start();

? ? }

}

classProducerextendsThread{

privateBlockingQueue b;

publicProducer(BlockingQueue<string> b){

this.b=b;

? ? }

publicsynchronizedvoidrun(){

String [] str=newString[] {

"java",

"struts",

"Spring"

? ? ? ? };

for(inti=0;i<9999999;i++) {

System.out.println(getName()+"生產(chǎn)者準(zhǔn)備生產(chǎn)集合元素再悼!");

try{

b.put(str[i%3]);

sleep(1000);

// 嘗試放入元素核畴,如果隊列已滿,則線程被阻塞

}catch(Exception e) {

? ? ? ? ? ? ? ? System.out.println(e);

? ? ? ? ? ? }

System.out.println(getName()+"生產(chǎn)完成:"+b);

? ? ? ? }

? ? }

}

classConsumerextendsThread{

privateBlockingQueue b;

publicConsumer(BlockingQueue<string> b){

this.b=b;

? ? }

publicsynchronizedvoidrun(){

while(true) {

System.out.println(getName()+"消費者準(zhǔn)備消費集合元素冲九!");

try{

sleep(1000);

// 嘗試取出元素谤草,如果隊列已空,則線程被阻塞

? ? ? ? ? ? ? ? b.take();

}catch(Exception e) {

? ? ? ? ? ? ? ? System.out.println(e);

? ? ? ? ? ? }

System.out.println(getName()+"消費完:"+b);

? ? ? ? }

? ? }

}

線程池

合理利用線程池能夠帶來三個好處:

降低資源消耗莺奸。通過重復(fù)利用已創(chuàng)建的線程降低線程創(chuàng)建和銷毀造成的消耗丑孩。

提高響應(yīng)速度。當(dāng)任務(wù)到達(dá)時灭贷,任務(wù)可以不需要等到線程創(chuàng)建就能立即執(zhí)行温学。

提高線程的可管理性。線程是稀缺資源甚疟,如果無限制的創(chuàng)建仗岖,不僅會消耗系統(tǒng)資源,還會降低系統(tǒng)的穩(wěn)定性览妖,使用線程池可以進(jìn)行統(tǒng)一的分配轧拄,調(diào)優(yōu)和監(jiān)控。

使用 Executors 工廠類產(chǎn)生線程池

Executor 線程池框架的最大優(yōu)點是把任務(wù)的提交和執(zhí)行解耦讽膏〗襞粒客戶端將要執(zhí)行的任務(wù)封裝成 Task,然后提交即可桅打。而 Task 如何執(zhí)行客戶端則是透明的是嗜。具體點講,提交一個 Callable 對象給ExecutorService(如:最常用的線程池 ThreadPoolExecutor )挺尾,將得到一個 Future 對象鹅搪,調(diào)用 Future 對象的 get 方法等待執(zhí)行結(jié)果。線程池實現(xiàn)原理類結(jié)構(gòu)圖如下:

上圖中涉及到的線程池內(nèi)部實現(xiàn)原理的所有類遭铺,不利于我們理解線程池如何使用丽柿。我們先從客戶端的角度出發(fā)恢准,看看客戶端使用線程池所涉及到的類結(jié)構(gòu)圖:由上圖可知,ExecutorService 是 Java 中對線程池定義的一個接口甫题,它 java.util.concurrent 包中馁筐。Java API 對 ExecutorService 接口的實現(xiàn)有兩個,所以這兩個即是Java線程池具體實現(xiàn)類如下:

ThreadPoolExecutor

ScheduledThreadPoolExecutor

除此之外坠非,ExecutorService 還繼承了 Executor 接口(注意區(qū)分 Executor 接口 和 Executors 工廠類)敏沉,這個接口只有一個 execute() 方法,最后我們看一下整個繼承樹:

使用 Executors 執(zhí)行多線程任務(wù)的步驟如下:

調(diào)用 Executors 類的靜態(tài)工廠方法創(chuàng)建一個 ExecutorService 對象炎码,該對象代表一個線程池盟迟。

創(chuàng)建 Runnable 實現(xiàn)類或 Callable 實現(xiàn)類的實例,作為線程執(zhí)行任務(wù)潦闲。

調(diào)用 ExecutorService 對象的 submit() 方法來提交 Runnable 實例或 Callable 實例攒菠。

當(dāng)不想提交任務(wù)時,調(diào)用 ExecutorService 對象的 shutdown() 方法來關(guān)閉線程池歉闰。

1. 使用 Executors 的靜態(tài)工廠類創(chuàng)建線程池的方法如下:?(1)newFixedThreadPool() : 作用:該方法返回一個固定線程數(shù)量的線程池辖众,該線程池中的線程數(shù)量始終不變,即不會再創(chuàng)建新的線程和敬,也不會銷毀已經(jīng)創(chuàng)建好的線程赵辕,自始自終都是那幾個固定的線程在工作,所以該線程池可以控制線程的最大并發(fā)數(shù)概龄。

栗子:假如有一個新任務(wù)提交時还惠,線程池中如果有空閑的線程則立即使用空閑線程來處理任務(wù),如果沒有私杜,則會把這個新任務(wù)存在一個任務(wù)隊列中蚕键,一旦有線程空閑了,則按 FIFO 方式處理任務(wù)隊列中的任務(wù)衰粹。

(2)newCachedThreadPool() : 作用:該方法返回一個可以根據(jù)實際情況調(diào)整線程池中線程的數(shù)量的線程池锣光。即該線程池中的線程數(shù)量不確定,是根據(jù)實際情況動態(tài)調(diào)整的铝耻。

栗子:假如該線程池中的所有線程都正在工作誊爹,而此時有新任務(wù)提交,那么將會創(chuàng)建新的線程去處理該任務(wù)瓢捉,而此時假如之前有一些線程完成了任務(wù)频丘,現(xiàn)在又有新任務(wù)提交,那么將不會創(chuàng)建新線程去處理泡态,而是復(fù)用空閑的線程去處理新任務(wù)搂漠。那么此時有人有疑問了,那這樣來說該線程池的線程豈不是會越集越多某弦?其實并不會桐汤,因為線程池中的線程都有一個“保持活動時間”的參數(shù)而克,通過配置它,如果線程池中的空閑線程的空閑時間超過該“保存活動時間”則立刻停止該線程怔毛,而該線程池默認(rèn)的“保持活動時間”為 60s员萍。

(3)newSingleThreadExecutor() : 作用:該方法返回一個只有一個線程的線程池,即每次只能執(zhí)行一個線程任務(wù)拣度,多余的任務(wù)會保存到一個任務(wù)隊列中碎绎,等待這一個線程空閑,當(dāng)這個線程空閑了再按 FIFO 方式順序執(zhí)行任務(wù)隊列中的任務(wù)蜡娶。

(4)newScheduledThreadPool() : 作用:該方法返回一個可以控制線程池內(nèi)線程定時或周期性執(zhí)行某任務(wù)的線程池。

(5)newSingleThreadScheduledExecutor() : 作用:該方法返回一個可以控制線程池內(nèi)線程定時或周期性執(zhí)行某任務(wù)的線程池映穗。只不過和上面的區(qū)別是該線程池大小為 1窖张,而上面的可以指定線程池的大小。

注意:?Executors 只是一個工廠類蚁滋,它所有的方法返回的都是T hreadPoolExecutor、ScheduledThreadPoolExecutor 這兩個類的實例。

2. ExecutorService 有如下幾個執(zhí)行方法:

- execute(Runnable)

- submit(Runnable)

- submit(Callable)

- invokeAny(...)

- invokeAll(...)

(1)execute(Runnable): 這個方法接收一個 Runnable 實例荷荤,并且異步的執(zhí)行缓熟,請看下面的實例:

ExecutorService executorService = Executors.newSingleThreadExecutor();

executorService.execute(newRunnable() {

publicvoidrun(){

System.out.println("Asynchronous task");

? ? }

});

executorService.shutdown();

(2)submit(Runnable): submit(Runnable) 和 execute(Runnable) 區(qū)別是前者可以返回一個 Future 對象,通過返回的 Future 對象走诞,我們可以檢查提交的任務(wù)是否執(zhí)行完畢副女,請看下面執(zhí)行的例子:

Future future = executorService.submit(newRunnable() {

publicvoidrun(){

System.out.println("Asynchronous task");

? ? }

});

future.get();// returns null if the task has finished correctly.

注意:?如果任務(wù)執(zhí)行完成,future.get() 方法會返回一個 null蚣旱。注意碑幅,future.get() 方法會產(chǎn)生阻塞。

(3)submit(Callable): submit(Callable) 和 submit(Runnable) 類似塞绿,也會返回一個 Future 對象沟涨,但是除此之外,submit(Callable) 接收的是一個 Callable 的實現(xiàn)异吻,Callable 接口中的 call() 方法有一個返回值裹赴,可以返回任務(wù)的執(zhí)行結(jié)果,而 Runnable 接口中的 run() 方法是 void 的诀浪,沒有返回值棋返。請看下面實例:

Future future = executorService.submit(newCallable(){

publicObjectcall()throwsException{

System.out.println("Asynchronous Callable");

return"Callable Result";

? ? }

});

System.out.println("future.get() = "+ future.get());

注意:?如果任務(wù)執(zhí)行完成,future.get() 方法會返回 Callable 任務(wù)的執(zhí)行結(jié)果雷猪。另外懊昨,future.get() 方法會產(chǎn)生阻塞。

(4)invokeAny(…): invokeAny(...) 方法接收的是一個 Callable 的集合春宣,執(zhí)行這個方法不會返回 Future酵颁,但是會返回所有 Callable 任務(wù)中其中一個任務(wù)的執(zhí)行結(jié)果嫉你。這個方法也無法保證返回的是哪個任務(wù)的執(zhí)行結(jié)果,反正是其中的某一個躏惋。請看下面實例:

ExecutorService executorService = Executors.newSingleThreadExecutor();

Set> callables =newHashSet>();

callables.add(newCallable() {

publicStringcall()throwsException{

return"Task 1";

? ? }

});

callables.add(newCallable() {

publicStringcall()throwsException{

return"Task 2";

? ? }

});

callables.add(newCallable() {

publicStringcall()throwsException{

return"Task 3";

? ? }

});

String result = executorService.invokeAny(callables);

System.out.println("result = "+ result);

executorService.shutdown();

大家可以嘗試執(zhí)行上面代碼幽污,每次執(zhí)行都會返回一個結(jié)果,并且返回的結(jié)果是變化的簿姨,可能會返回“Task2”距误,也可是“Task1”或者其它。

(5)invokeAll(…): invokeAll(...) 與 invokeAny(...) 類似也是接收一個 Callable 集合扁位,但是前者執(zhí)行之后會返回一個 Future 的 List准潭,其中對應(yīng)著每個 Callable 任務(wù)執(zhí)行后的 Future 對象。情況下面這個實例:

ExecutorService executorService = Executors.newSingleThreadExecutor();

Set> callables =newHashSet>();

callables.add(newCallable() {

publicStringcall()throwsException{

return"Task 1";

? ? }

});

callables.add(newCallable() {

publicStringcall()throwsException{

return"Task 2";

? ? }

});

callables.add(newCallable() {

publicStringcall()throwsException{

return"Task 3";

? ? }

});

List<future<string>> futures = executorService.invokeAll(callables);

for(Future future : futures) {

System.out.println("future.get = "+ future.get());

}

executorService.shutdown();

3. ExecutorService 關(guān)閉方法?當(dāng)我們使用完成 ExecutorService 之后應(yīng)該關(guān)閉它域仇,否則它里面的線程會一直處于運行狀態(tài)刑然。舉個例子,如果的應(yīng)用程序是通過 main() 方法啟動的暇务,在這個 main() 退出之后泼掠,如果應(yīng)用程序中的 ExecutorService 沒有關(guān)閉,這個應(yīng)用將一直運行垦细。之所以會出現(xiàn)這種情況择镇,是因為 ExecutorService 中運行的線程會阻止 JVM 關(guān)閉。

要關(guān)閉 ExecutorService 中執(zhí)行的線程括改,我們可以調(diào)用 ExecutorService.shutdown() 方法腻豌。在調(diào)用 shutdown() 方法之后,ExecutorService 不會立即關(guān)閉嘱能,但是它不再接收新的任務(wù)饲梭,直到當(dāng)前所有線程執(zhí)行完成才會關(guān)閉,所有在 shutdown() 執(zhí)行之前提交的任務(wù)都會被執(zhí)行焰檩。

如果想立即關(guān)閉 ExecutorService憔涉,我們可以調(diào)用 ExecutorService.shutdownNow() 方法。這個動作將跳過所有正在執(zhí)行的任務(wù)和被提交還沒有執(zhí)行的任務(wù)析苫。但是它并不對正在執(zhí)行的任務(wù)做任何保證兜叨,有可能它們都會停止,也有可能執(zhí)行完成衩侥。

使用 Java 8 增強(qiáng)的 ForkJoinPool 產(chǎn)生線程池

在 Java 8 中国旷,引入了自動并行化的概念。它能夠讓一部分 Java 代碼自動地以并行的方式執(zhí)行茫死,前提是使用了 ForkJoinPool跪但。

ForkJoinPool 同 ThreadPoolExecutor 一樣,也實現(xiàn)了 Executor 和 ExecutorService 接口峦萎。它使用了一個無限隊列來保存需要執(zhí)行的任務(wù)屡久,而線程的數(shù)量則是通過構(gòu)造函數(shù)傳入忆首,如果沒有向構(gòu)造函數(shù)中傳入希望的線程數(shù)量,那么當(dāng)前計算機(jī)可用的 CPU 數(shù)量會被設(shè)置為線程數(shù)量作為默認(rèn)值被环。

ForkJoinPool 主要用來使用分治法(Divide-and-Conquer Algorithm)來解決問題糙及。典型的應(yīng)用比如快速排序算法。這里的要點在于筛欢,F(xiàn)orkJoinPool 需要使用相對少的線程來處理大量的任務(wù)浸锨。比如要對 1000 萬個數(shù)據(jù)進(jìn)行排序,那么會將這個任務(wù)分割成兩個 500 萬的排序任務(wù)和一個針對這兩組 500 萬數(shù)據(jù)的合并任務(wù)版姑。以此類推柱搜,對于 500 萬的數(shù)據(jù)也會做出同樣的分割處理,到最后會設(shè)置一個閾值來規(guī)定當(dāng)數(shù)據(jù)規(guī)模到多少時剥险,停止這樣的分割處理聪蘸。比如,當(dāng)元素的數(shù)量小于 10 時炒嘲,會停止分割宇姚,轉(zhuǎn)而使用插入排序?qū)λ鼈冞M(jìn)行排序匈庭。那么到最后夫凸,所有的任務(wù)加起來會有大概 2000000+ 個。問題的關(guān)鍵在于阱持,對于一個任務(wù)而言夭拌,只有當(dāng)它所有的子任務(wù)完成之后,它才能夠被執(zhí)行衷咽。所以當(dāng)使用 ThreadPoolExecutor 時鸽扁,使用分治法會存在問題,因為 ThreadPoolExecutor 中的線程無法像任務(wù)隊列中再添加一個任務(wù)并且在等待該任務(wù)完成之后再繼續(xù)執(zhí)行镶骗。而使用 ForkJoinPool 時桶现,就能夠讓其中的線程創(chuàng)建新的任務(wù),并掛起當(dāng)前的任務(wù)鼎姊,此時線程就能夠從隊列中選擇子任務(wù)執(zhí)行骡和。比如,我們需要統(tǒng)計一個 double 數(shù)組中小于 0.5 的元素的個數(shù)相寇,那么可以使用 ForkJoinPool 進(jìn)行實現(xiàn)如下:

publicclassForkJoinTest{

privatedouble[] d;

privateclassForkJoinTaskextendsRecursiveTask{

privateintfirst;

privateintlast;

publicForkJoinTask(intfirst,intlast){

this.first = first;

this.last = last;

? ? ? ? }

protectedIntegercompute(){

intsubCount;

if(last - first <10) {

subCount =0;

for(inti = first; i <= last; i++) {

if(d[i] <0.5){

? ? ? ? ? ? ? ? ? ? ? ? subCount++;

? ? ? ? ? ? ? ? ? ? }

? ? ? ? ? ? ? ? }

}else{

intmid = (first + last) /2;

ForkJoinTask left =newForkJoinTask(first, mid);

? ? ? ? ? ? ? ? left.fork();

ForkJoinTask right =newForkJoinTask(mid +1, last);

? ? ? ? ? ? ? ? right.fork();

? ? ? ? ? ? ? ? subCount = left.join();

? ? ? ? ? ? ? ? subCount += right.join();

? ? ? ? ? ? }

returnsubCount;

? ? ? ? }

? ? }

publicstaticvoidmain(String[] args){

ForkJoinPool pool=newForkJoinPool();

pool.submit(newForkJoinTask(0,9999999));

pool.awaitTermination(2,TimeUnit.SECONDS);

System.out.println("Found "+ n +" values");

? ? }

}

以上的關(guān)鍵是 fork() 和 join() 方法慰于。在 ForkJoinPool 使用的線程中,會使用一個內(nèi)部隊列來對需要執(zhí)行的任務(wù)以及子任務(wù)進(jìn)行操作來保證它們的執(zhí)行順序唤衫。

注意:?使用 ThreadPoolExecutor 和 ForkJoinPool 的性能差異:

首先婆赠,使用 ForkJoinPool 能夠使用數(shù)量有限的線程來完成非常多的具有父子關(guān)系的任務(wù),比如使用4個線程來完成超過 200 萬個任務(wù)佳励。但是休里,使用 ThreadPoolExecutor 時蛆挫,是不可能完成的,因為 ThreadPoolExecutor 中的 Thread 無法選擇優(yōu)先執(zhí)行子任務(wù)份帐,需要完成 200 萬個具有父子關(guān)系的任務(wù)時璃吧,也需要 200 萬個線程,顯然這是不可行的废境。

ForkJoinPool 能夠?qū)崿F(xiàn)工作竊刃蟀ぁ(Work Stealing),在該線程池的每個線程中會維護(hù)一個隊列來存放需要被執(zhí)行的任務(wù)噩凹。當(dāng)線程自身隊列中的任務(wù)都執(zhí)行完畢后巴元,它會從別的線程中拿到未被執(zhí)行的任務(wù)并幫助它執(zhí)行。因此驮宴,提高了線程的利用率逮刨,從而提高了整體性能。

對于 ForkJoinPool堵泽,還有一個因素會影響它的性能修己,就是停止進(jìn)行任務(wù)分割的那個閾值。比如在之前的快速排序中迎罗,當(dāng)剩下的元素數(shù)量小于10的時候睬愤,就會停止子任務(wù)的創(chuàng)建。

結(jié)論:

當(dāng)需要處理遞歸分治算法時纹安,考慮使用 ForkJoinPool尤辱。

仔細(xì)設(shè)置不再進(jìn)行任務(wù)劃分的閾值,這個閾值對性能有影響厢岂。

Java 8 中的一些特性會使用到 ForkJoinPool 中的通用線程池光督。在某些場合下,需要調(diào)整該線程池的默認(rèn)的線程數(shù)量塔粒。

死鎖

產(chǎn)生死鎖的四個必要條件如下结借。當(dāng)下邊的四個條件都滿足時即產(chǎn)生死鎖,即任意一個條件不滿足既不會產(chǎn)生死鎖卒茬。

死鎖的四個必要條件:

互斥條件:資源不能被共享船老,只能被同一個進(jìn)程使用。

請求與保持條件:已經(jīng)得到資源的進(jìn)程可以申請新的資源扬虚。

非剝奪條件:已經(jīng)分配的資源不能從相應(yīng)的進(jìn)程中被強(qiáng)制剝奪努隙。

循環(huán)等待條件:系統(tǒng)中若干進(jìn)程組成環(huán)路,該環(huán)路中每個進(jìn)程都在等待相鄰進(jìn)程占用的資源辜昵。

舉個常見的死鎖例子:進(jìn)程 A 中包含資源 A荸镊,進(jìn)程 B 中包含資源 B,A 的下一步需要資源 B,B 的下一步需要資源 A躬存,所以它們就互相等待對方占有的資源釋放张惹,所以也就產(chǎn)生了一個循環(huán)等待死鎖。

處理死鎖的方法:

忽略該問題岭洲,也即鴕鳥算法宛逗。當(dāng)發(fā)生了什么問題時,不管他盾剩,直接跳過雷激,無視它。

檢測死鎖并恢復(fù)告私。

資源進(jìn)行動態(tài)分配屎暇。

破除上面的四種死鎖條件之一。

線程相關(guān)類

ThreadLocal?ThreadLocal 它并不是一個線程驻粟,而是一個可以在每個線程中存儲數(shù)據(jù)的數(shù)據(jù)存儲類根悼,通過它可以在指定的線程中存儲數(shù)據(jù),數(shù)據(jù)存儲之后蜀撑,只有在指定線程中可以獲取到存儲的數(shù)據(jù)挤巡,對于其他線程來說則無法獲取到該線程的數(shù)據(jù)。 即多個線程通過同一個 ThreadLocal 獲取到的東西是不一樣的酷麦,就算有的時候出現(xiàn)的結(jié)果是一樣的(偶然性矿卑,兩個線程里分別存了兩份相同的東西),但他們獲取的本質(zhì)是不同的贴铜。使用這個工具類可以簡化多線程編程時的并發(fā)訪問粪摘,很簡潔的隔離多線程程序的競爭資源瀑晒。

對于多線程資源共享的問題绍坝,同步機(jī)制采用了“以時間換空間”的方式,而 ThreadLocal 采用了“以空間換時間”的方式苔悦。前者僅提供一份變量轩褐,讓不同的線程排隊訪問,而后者為每一個線程都提供了一份變量玖详,因此可以同時訪問而互不影響把介。ThreadLocal 類提供了如下的三個 public 方法:

// 創(chuàng)建一個線程本地變量

ThreadLocal()

// 返回此線程局部變量的當(dāng)前線程副本中的值,如果這是線程第一次調(diào)用該方法蟋座,則創(chuàng)建并初始化此副本

Tget()

// 返回此線程局部變量的當(dāng)前線程的初始值

protectedTinitialValue()

下面通過系統(tǒng)源碼來分析出現(xiàn)這個結(jié)果的原因拗踢。 在 ThreadLocal 中存在著兩個很重要的方法,get() 和 set() 方法向臀,一個讀取一個設(shè)置巢墅。

/**

* Returns the value of this variable for the current thread. If an entry

* doesn't yet exist for this variable on this thread, this method will

* create an entry, populating the value with the result of

* {@link#initialValue()}.

*

*@returnthe current value of the variable for the calling thread.

*/

@SuppressWarnings("unchecked")

publicTget(){

// Optimized for the fast path.

? Thread currentThread = Thread.currentThread();

? Values values = values(currentThread);

if(values !=null) {

? ? ? ? Object[] table = values.table;

intindex = hash & values.mask;

if(this.reference == table[index]) {

return(T) table[index +1];

? ? ? ? }

}else{

    values = initializeValues(currentThread);

  }

return(T) values.getAfterMiss(this);

}

/**

* Sets the value of this variable for the current thread. If set to

* {@codenull}, the value will be set to null and the underlying entry will

* still be present.

*

*@paramvalue the new value of the variable for the caller thread.

*/

publicvoidset(T value){

? ? Thread currentThread = Thread.currentThread();

   Values values = values(currentThread);

if(values ==null) {

    ? values = initializeValues(currentThread);

   }

values.put(this, value);

}

從注釋上可以看出,get() 方法會返回一個當(dāng)前線程的變量值,如果數(shù)組不存在就會創(chuàng)建一個新的君纫。另外驯遇,對于“當(dāng)前線程”和“數(shù)組”,數(shù)組對于每個線程來說都是不同的 values.table蓄髓。而 values 是通過當(dāng)前線程獲取到的一個 Values 對象叉庐,因此這個數(shù)組是每個線程唯一的,不能共用会喝,而下面的幾句話也更直接了陡叠,獲取一個索引,再返回通過這個索引找到數(shù)組中對應(yīng)的值肢执。這也就解釋了為什么多個線程通過同一個 ThreadLocal 返回的是不同的東西匾竿。

Java 中為什么要這么設(shè)置呢?

ThreadLocal 在日常開發(fā)中使用到的地方較少蔚万,但是在某些特殊的場景下岭妖,通過 ThreadLocal 可以輕松實現(xiàn)一些看起來很復(fù)雜的功能。一般來說反璃,當(dāng)某些數(shù)據(jù)是以線程為作用域并且不同線程具有不同的數(shù)據(jù)副本的時候昵慌,就可以考慮使用 ThreadLocal。例如在 Handler 和 Looper 中淮蜈。對于 Handler 來說斋攀,它需要獲取當(dāng)前線程的 Looper,很顯然 Looper 的作用域就是線程并且不同的線程具有不同的 Looper梧田,這個時候通過 ThreadLocal 就可以輕松的實現(xiàn) Looper 在線程中的存取淳蔼。如果不采用 ThreadLocal,那么系統(tǒng)就必須提供一個全局的哈希表供 Handler 查找指定的 Looper裁眯,這樣就比較麻煩了鹉梨,還需要一個管理類。

ThreadLocal 的另一個使用場景是復(fù)雜邏輯下的對象傳遞穿稳,比如監(jiān)聽器的傳遞存皂,有些時候一個線程中的任務(wù)過于復(fù)雜,就可能表現(xiàn)為函數(shù)調(diào)用棧比較深以及代碼入口的多樣性逢艘,這種情況下旦袋,我們又需要監(jiān)聽器能夠貫穿整個線程的執(zhí)行過程。這個時候就可以使用到 ThreadLocal它改,通過 ThreadLocal 可以讓監(jiān)聽器作為線程內(nèi)的全局對象存在疤孕,在線程內(nèi)通過 get() 方法就可以獲取到監(jiān)聽器。如果不采用的話央拖,可以使用參數(shù)傳遞祭阀,但是這種方式在設(shè)計上不是特別好截亦,當(dāng)調(diào)用棧很深的時候,通過參數(shù)來傳遞監(jiān)聽器這個設(shè)計太糟糕柬讨。而另外一種方式就是使用 static 靜態(tài)變量的方式崩瓤,但是這種方式存在一定的局限性,拓展性并不是特別的強(qiáng)踩官。比如有 10 個線程在執(zhí)行却桶,就需要提供 10 個監(jiān)聽器對象。

注意:?ThreadLocal 和其他所有的同步機(jī)制一樣蔗牡,都是為了解決多線程中對于同一變量的訪問沖突颖系。值普通的同步機(jī)制中,通過對象加鎖來實現(xiàn)多線程對同一變量的安全訪問辩越,且該變量是多線程共享的嘁扼,所有需要使用這種同步機(jī)制來明確分開是在什么時候?qū)ψ兞窟M(jìn)行讀寫,在什么時候需要鎖定該對象黔攒。此種情況下趁啸,系統(tǒng)并沒有將這個資源復(fù)制多份,而是采取安全機(jī)制來控制訪問而已督惰。 ThreadLocal 只是從另一個角度解決多線程的并發(fā)訪問不傅,即將需要并發(fā)訪問的資源復(fù)制多份,每個線程擁有一份資源赏胚,每個線程都有自己的資源副本访娶。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市觉阅,隨后出現(xiàn)的幾起案子崖疤,更是在濱河造成了極大的恐慌,老刑警劉巖典勇,帶你破解...
    沈念sama閱讀 211,123評論 6 490
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件劫哼,死亡現(xiàn)場離奇詭異,居然都是意外死亡痴柔,警方通過查閱死者的電腦和手機(jī)沦偎,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,031評論 2 384
  • 文/潘曉璐 我一進(jìn)店門疫向,熙熙樓的掌柜王于貴愁眉苦臉地迎上來咳蔚,“玉大人,你說我怎么就攤上這事搔驼√富穑” “怎么了?”我有些...
    開封第一講書人閱讀 156,723評論 0 345
  • 文/不壞的土叔 我叫張陵舌涨,是天一觀的道長糯耍。 經(jīng)常有香客問我,道長,這世上最難降的妖魔是什么温技? 我笑而不...
    開封第一講書人閱讀 56,357評論 1 283
  • 正文 為了忘掉前任革为,我火速辦了婚禮,結(jié)果婚禮上舵鳞,老公的妹妹穿的比我還像新娘震檩。我一直安慰自己,他們只是感情好蜓堕,可當(dāng)我...
    茶點故事閱讀 65,412評論 5 384
  • 文/花漫 我一把揭開白布抛虏。 她就那樣靜靜地躺著,像睡著了一般套才。 火紅的嫁衣襯著肌膚如雪迂猴。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 49,760評論 1 289
  • 那天背伴,我揣著相機(jī)與錄音沸毁,去河邊找鬼。 笑死傻寂,一個胖子當(dāng)著我的面吹牛以清,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播崎逃,決...
    沈念sama閱讀 38,904評論 3 405
  • 文/蒼蘭香墨 我猛地睜開眼掷倔,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了个绍?” 一聲冷哼從身側(cè)響起勒葱,我...
    開封第一講書人閱讀 37,672評論 0 266
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎巴柿,沒想到半個月后凛虽,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 44,118評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡广恢,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,456評論 2 325
  • 正文 我和宋清朗相戀三年凯旋,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片钉迷。...
    茶點故事閱讀 38,599評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡至非,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出糠聪,到底是詐尸還是另有隱情荒椭,我是刑警寧澤,帶...
    沈念sama閱讀 34,264評論 4 328
  • 正文 年R本政府宣布舰蟆,位于F島的核電站趣惠,受9級特大地震影響狸棍,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜味悄,卻給世界環(huán)境...
    茶點故事閱讀 39,857評論 3 312
  • 文/蒙蒙 一草戈、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧侍瑟,春花似錦猾瘸、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,731評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至咐低,卻和暖如春揽思,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背见擦。 一陣腳步聲響...
    開封第一講書人閱讀 31,956評論 1 264
  • 我被黑心中介騙來泰國打工钉汗, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人鲤屡。 一個月前我還...
    沈念sama閱讀 46,286評論 2 360
  • 正文 我出身青樓损痰,卻偏偏與公主長得像,于是被迫代替她去往敵國和親酒来。 傳聞我的和親對象是個殘疾皇子卢未,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 43,465評論 2 348