三、終結(jié)任務(wù)
1. 在阻塞時(shí)終結(jié)
線程狀態(tài)
一個(gè)線程可以處于以下四種狀態(tài)之一:
- 1)新建(new):當(dāng)線程被創(chuàng)建時(shí),他只會(huì)短暫的處于這種狀態(tài)。此時(shí),他已經(jīng)分配了必須的系統(tǒng)資源,并執(zhí)行了初始化。此刻線程已經(jīng)有資格獲得CPU時(shí)間了,之后調(diào)度器將把這個(gè)線程轉(zhuǎn)變?yōu)榭蛇\(yùn)行zhuang't狀態(tài)或阻塞狀態(tài)。
- 2)就緒(Runnable):在這種狀態(tài)下,只要調(diào)度器把時(shí)間片分配給線程,線程就可以運(yùn)行。也就是說(shuō),在任意時(shí)刻,線程可以運(yùn)行也可以不運(yùn)行。只要調(diào)度器能分配時(shí)間片給線程,他就可以運(yùn)行,這不同于死亡和阻塞狀態(tài)。
- 3)阻塞(Blocked):線程能夠運(yùn)行,但是某個(gè)條件阻止他的運(yùn)行。當(dāng)線程處于阻塞狀態(tài)時(shí),調(diào)度器將忽略線程,不會(huì)分配給線程任何CPU時(shí)間。直到線程重新進(jìn)入就緒狀態(tài),他才可能執(zhí)行操作。
- 4)死亡(Dead):處于死亡或終止?fàn)顟B(tài)的線程將是不可調(diào)度的,并且再也不會(huì)得到CPU時(shí)間,它的任務(wù)已結(jié)束,或不再是可運(yùn)行的。任務(wù)死亡的通常方式是從run()方法返回,但是任務(wù)的線程還可以被中斷。
進(jìn)入阻塞狀態(tài)
一個(gè)任務(wù)進(jìn)入阻塞狀態(tài),可能有如下原因:
- 1)通過(guò)調(diào)用sleep()使任務(wù)進(jìn)入休眠狀態(tài),在這種情況下,任務(wù)在指定的時(shí)間內(nèi)不會(huì)運(yùn)行。
- 2)調(diào)用wait()使線程掛起,直到線程得到了notify()或notifyAll()消息(或者concurrent類庫(kù)中等價(jià)的signal()或signalAll()消息),線程才會(huì)進(jìn)入就緒狀態(tài)。
- 3)任務(wù)在等待某個(gè)輸入/輸出完成。
- 4)任務(wù)試圖在某個(gè)對(duì)象上調(diào)用其同步控制方法,但是對(duì)象鎖不可用,因?yàn)榱硪粋€(gè)任務(wù)已經(jīng)獲取了這個(gè)鎖。
四、線程之間的協(xié)作
當(dāng)使用線程來(lái)同時(shí)運(yùn)行多個(gè)任務(wù)時(shí),可以通過(guò)使用鎖(互斥)來(lái)同步兩個(gè)任務(wù)的行為,從而使得一個(gè)任務(wù)不會(huì)干涉另一個(gè)任務(wù)的資源。也就是說(shuō),如果兩個(gè)任務(wù)在交替著步入某向資源(通常是內(nèi)存),你可以使用互斥來(lái)使得任何時(shí)刻只有一個(gè)任務(wù)可以訪問(wèn)這項(xiàng)資源。
當(dāng)任務(wù)協(xié)作時(shí),關(guān)鍵問(wèn)題是這些任務(wù)之間的握手。為了實(shí)現(xiàn)這種握手,我們使用了相同的基礎(chǔ)特性:互斥。在這種情況下,互斥能夠確保只有一個(gè)任務(wù)可以響應(yīng)某個(gè)信號(hào),這樣就可以根除任何可能的競(jìng)爭(zhēng)條件。在互斥之上,我們?yōu)槿蝿?wù)添加了一種途徑,可以將其自身掛起,直到某些外部條件發(fā)生變化,表示是時(shí)候讓這個(gè)任務(wù)向前開(kāi)動(dòng)了為止。
1. wait()和notifyAll()
wait()使你可以等待某個(gè)條件發(fā)生變化,而改變這個(gè)條件超出了當(dāng)前方法的控制能力。通常,這種條件將由另一個(gè)任務(wù)來(lái)改變。你肯定不想在你的任務(wù)測(cè)試這個(gè)條件的同時(shí),不斷地進(jìn)行空循環(huán),這被稱為忙等待,通暢是一種不良的CPU周期使用方式。因此wait()會(huì)在等待外部世界產(chǎn)生變化的時(shí)候?qū)⑷蝿?wù)掛起,并且只有在notify()或notifyAll()發(fā)生時(shí),即表示發(fā)生了某些感興趣的事物,這個(gè)任務(wù)才會(huì)被喚醒并去檢查所產(chǎn)生的變化。因此,wait()提供了一種在任務(wù)之間對(duì)活動(dòng)同步的形式。
調(diào)用sleep()的時(shí)候鎖并沒(méi)有被釋放,調(diào)用yield()也屬于這種情況,理解這一點(diǎn)很重要。另一方面,當(dāng)一個(gè)任務(wù)在方法里遇到了對(duì)wait()的調(diào)用的時(shí)候,線程的執(zhí)行被掛起,對(duì)象上的鎖被釋放。因?yàn)閣ait()將釋放鎖,這就意味著另一個(gè)任務(wù)可以獲得這個(gè)鎖,因此在該對(duì)象(現(xiàn)在是未鎖定的)中的其他synchronized方法可以在wait()期間被調(diào)用。這點(diǎn)至關(guān)重要。因?yàn)檫@些其他的方法通常將會(huì)產(chǎn)生改變,而這種改變正是使被掛起的任務(wù)重新喚醒所感興趣的變化。
有兩種形式的wait()。第一種版本接受毫秒數(shù)作為參數(shù),含義與sleep()方法里的參數(shù)意思相同,都是指“在此期間暫停”。但是與sleep()不同的是,對(duì)于wait()而言:
- 1)在wait()期間對(duì)象鎖是釋放的。
- 2)可以通過(guò)notify()、notifyAll(),或者令時(shí)間到期,從wait()中恢復(fù)執(zhí)行。
第二種,也是更常用形式的wait()不接受任何參數(shù)。這種wait()將無(wú)限等待下去,直到線程接收到notify()或者notifyAll()消息。
wait()、notify()以及notifyAll()有一個(gè)比較特殊的方面,那就是這些方法是基類Object的一部分,而不屬于Threadde的一部分。所以你可以把wait()放進(jìn)任何同步控制方法里,而不用考慮這個(gè)類是繼承自Thread還是實(shí)現(xiàn)了Runnable接口。實(shí)際上,只能在同步控制方法或同步控制塊li里調(diào)用wait()、notify()和notifyAll()(因?yàn)椴挥貌僮麈i,所以sleep()可以在非同步控制方法里調(diào)用)。調(diào)用wait()、notify()和notifyAll()的任務(wù)在調(diào)用這些方法前必須擁有對(duì)象的鎖。
可以讓另一個(gè)對(duì)象執(zhí)行某種操作以維護(hù)其自己的鎖。要這么做的話,必須首先得到對(duì)象的鎖。比如,如果要向?qū)ο髕發(fā)送notifyAll(),就必須在能夠取得x的鎖的同步控制塊中這么做:
synchronized(x){
x.notifyAll();
}
我們看一下示例,WaxOMatic.java有兩個(gè)過(guò)程,一個(gè)是將la蠟涂到Car上,一個(gè)是拋光它。拋光任務(wù)在涂蠟任務(wù)完成之前,是不能執(zhí)行其工作的,而涂蠟任務(wù)在涂另一層蠟之前,必須等待拋光任務(wù)完成。WaxOn和WaxOff都使用了Car對(duì)象,該對(duì)象在這些任務(wù)等待條件變化的時(shí)候,使用wait()和notifyAll()來(lái)掛起和重新啟動(dòng)這些任務(wù):
public class WaxOMatic {
public static void main(String[] args) throws Exception{
Car car = new Car();
ExecutorService exec = Executors.newCachedThreadPool();
exec.execute(new WaxOff(car));
exec.execute(new WaxOn(car));
TimeUnit.SECONDS.sleep(5);
exec.shutdown();
}
}
class Car {
private boolean waxOn = false;
public synchronized void waxed() {
waxOn = true;
notifyAll();
}
public synchronized void buffed() {
waxOn = false;
notifyAll();
}
public synchronized void waitForWaxing() throws InterruptedException {
while (waxOn == false) {
wait();
}
}
public synchronized void waitForBuffing() throws InterruptedException {
while (waxOn == true) {
wait();
}
}
}
class WaxOn implements Runnable {
private Car car;
public WaxOn(Car car) {
this.car = car;
}
@Override
public void run() {
try {
while (!Thread.interrupted()) {
System.out.println("wax on");
TimeUnit.MILLISECONDS.sleep(200);
car.waxed();
car.waitForBuffing();
}
}catch (InterruptedException e){
System.out.println("exiting via interrupt");
}
System.out.println("ending wax on task");
}
}
class WaxOff implements Runnable {
private Car car;
public WaxOff(Car car) {
this.car = car;
}
@Override
public void run() {
try {
while (!Thread.interrupted()) {
car.waitForWaxing();
System.out.println("wax off");
TimeUnit.MILLISECONDS.sleep(200);
car.buffed();
}
}catch (InterruptedException e){
System.out.println("exiting via interrupt");
}
System.out.println("ending wax off task");
}
}
錯(cuò)失的信號(hào)
當(dāng)兩個(gè)線程使用notify()/wait()或notifyAll()/wait()進(jìn)行協(xié)作時(shí),有可能會(huì)錯(cuò)過(guò)某個(gè)信號(hào),假設(shè)T1是通知T2的線程,而這兩個(gè)線程都是通過(guò)下面(有缺陷的)方式實(shí)現(xiàn)的:
T1:
synchronized(sharedMonitor){
<setup condition for T2>
sharedMonitor.notify();
}
T2:
while(someCondition){
//Point 1
synchronized(sharedMonitor){
sharedMonitor.wait();
}
}
<setup condition for T2>是防止T2調(diào)用wait()的一個(gè)動(dòng)作,當(dāng)然前提是T2還沒(méi)有調(diào)用wait()。
假設(shè)T2對(duì)someCondition求值并發(fā)現(xiàn)其為true。在Point1,線程調(diào)度器可能切換到了T1。而T1將執(zhí)行其設(shè)置,然后調(diào)用notify()。當(dāng)T2得以繼續(xù)執(zhí)行時(shí),此時(shí)對(duì)于T2來(lái)說(shuō),時(shí)機(jī)已經(jīng)晚了,以至于不能意識(shí)到這個(gè)條件已經(jīng)發(fā)生了變化,因此會(huì)盲目進(jìn)入wait()。此時(shí)notify()將錯(cuò)失,而T2也將無(wú)限的等待這個(gè)已經(jīng)發(fā)送過(guò)的信號(hào),從而發(fā)生死鎖。
該問(wèn)題的解決方案是防止在someCondition變量上產(chǎn)生競(jìng)爭(zhēng)條件。下面是T2正確的執(zhí)行方式:
synchronized(sharedMonitor){
while(someCondition){
sharedMonitor.wait();
}
}
現(xiàn)在,如果T1首先執(zhí)行,當(dāng)控制返回T2時(shí),它將發(fā)現(xiàn)條件發(fā)生了變化,從而不會(huì)進(jìn)入wait()。反過(guò)來(lái),如果T2首先執(zhí)行,那它將進(jìn)入wait(),并且稍后會(huì)由T1喚醒。因此,信號(hào)不會(huì)錯(cuò)失。
2. 生產(chǎn)者-消費(fèi)者與隊(duì)列
wait()和notifyAll()方法是一種非常低級(jí)的方式解決了任何互操作問(wèn)題,即每次交互shi時(shí)都握手。在許多情況下,你可以瞄向更高的抽象級(jí)別,使用同步隊(duì)列來(lái)解決任務(wù)協(xié)作問(wèn)題,同步隊(duì)列在任何時(shí)刻都只允許一個(gè)任務(wù)插入或移除元素。在java.util.concurrent.BlockingQueue接口中提供了這個(gè)隊(duì)列,這個(gè)接口有大量的標(biāo)準(zhǔn)實(shí)現(xiàn)。你通??梢允褂肔inkedBlockingQueue,他是一個(gè)無(wú)界隊(duì)列,還可以使用ArrayBlockingQueue,它具有固定的尺寸,因此你可以在他被阻塞之前,向其中放置有限數(shù)量的元素。
如果消費(fèi)者任務(wù)試圖從隊(duì)列中獲取對(duì)象,而該隊(duì)列此時(shí)為空,那么這些隊(duì)列還可以掛起消費(fèi)者任務(wù),并且當(dāng)有更多的元素可用時(shí)恢復(fù)消費(fèi)者任務(wù)。阻塞隊(duì)列可以解決非常大量的問(wèn)題,而其方式與wait()和notifyAll()相比,則簡(jiǎn)單可靠的多。
3. 死鎖
任務(wù)可以變成阻塞狀態(tài),所以就可能出現(xiàn)這種情況:某個(gè)任務(wù)在等待另一個(gè)任務(wù),而后者又等待別的任務(wù),這樣一直下去,直到這條鏈條上的任務(wù)又在等待第一個(gè)任務(wù)釋放鎖。這得到了一個(gè)任務(wù)之間等待的連續(xù)循環(huán),沒(méi)有哪個(gè)線程能繼續(xù),這被稱為死鎖。
要修正死鎖問(wèn)題,你必須明白,當(dāng)以下四個(gè)條件同時(shí)滿足時(shí),就會(huì)發(fā)生死鎖:
- 1)互斥條件。任務(wù)使用的資源中至少有一個(gè)是不能共享的。
- 2)至少有一個(gè)任務(wù)它必須持有一個(gè)資源且正在等待獲取一個(gè)當(dāng)前被別的任務(wù)持有的資源。
- 3)資源不能被任務(wù)搶占,任務(wù)必須把資源釋放當(dāng)做普通事件。
- 4)必須有循環(huán)等待,這時(shí)一個(gè)任務(wù)等待其他任務(wù)所持有的資源,后者又在等待另一個(gè)任務(wù)所持有的資源,這樣一直等待下去,直到有一個(gè)任務(wù)在等待第一個(gè)任務(wù)所持有的資源,使得大家都被鎖住。
因?yàn)橐l(fā)生死鎖的話,所有這些條件必須全部滿足;所以要防止死鎖的話,只需破壞其中一個(gè)即可。