多線程協(xié)作的基本機(jī)制 wait/notify
多線程之間除了競爭訪問同一個資源外,也經(jīng)常需要相互協(xié)作授滓,怎么協(xié)作呢童本?本節(jié)就來介紹Java中多線程協(xié)作的基本機(jī)制 wait/notify。
wait 實(shí)際上做了什么呢乱凿?它在等待什么蚯撩?之前我們說過础倍,每個對象都有一把鎖和等待隊列,一個線程在進(jìn)入 synchronized 代碼塊時胎挎,會嘗試獲取鎖沟启,如果獲取不到則會把當(dāng)前線程加入等待隊列中,其實(shí)犹菇,除了用于鎖的等待隊列德迹,每個對象還有另一個等待隊列,表示條件隊列揭芍,該隊列用于線程間的協(xié)作胳搞。
notify 做的事情就是從條件隊列中選一個線程,將其從隊列中移除并喚醒,notify 和 notifyAll 的區(qū)別是肌毅,它會移除條件隊列中所有的線程并全部喚醒筷转。
wait/notify 方法只能在 synchronized 代碼塊內(nèi)被調(diào)用,如果調(diào)用 wait/notify 方法時悬而,當(dāng)前線程沒有持有對象鎖呜舒,會拋出異常 java.lang.IllegalMonitor-StateException。
同時開始
每個線程在開始前進(jìn)行 wait摊滔,然后主線程通過 notifyAll 喚醒所有阴绢。
同時結(jié)束
我們之前通過主線程等待子線程使用的是 join店乐,但是 join 有時比較麻煩艰躺,需要主線程逐一等待每個子線程。
主線程先等待眨八,只有等到所有子線程結(jié)束腺兴。然后一個條件,必須先 wait廉侧,再 notify页响。
異步結(jié)果
一種常見的模式是異步調(diào)用,異步調(diào)用返回一個一般稱為 Future 的對象段誊,通過它可以獲得最終的結(jié)果闰蚕。
package qy.basic.ch21;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
/**
* 線程池自定義任務(wù) 簡單 demo
*/
public class Ch21_10_Executor {
interface MyFuture<V> {
// 阻塞直到線程運(yùn)行結(jié)束
V get() throws InterruptedException, ExecutionException;
}
static class MyExecutor {
// 它封裝了創(chuàng)建子線程,同步獲取結(jié)果的過程连舍,它會創(chuàng)建一個執(zhí)行子線程
public <V> MyFuture<V> submit(final Callable<V> callable) {
Object lock = new Object();
ExecutorThread<V> thread = new ExecutorThread<>(callable, lock);
thread.start();
MyFuture<V> future = new MyFuture<V>() {
@Override
public V get() throws InterruptedException, ExecutionException {
synchronized (lock) {
while(!thread.isDone) {
lock.wait();
}
if (thread.getException() != null) {
throw new ExecutionException(thread.getException());
}
V v = thread.getResult();
return v;
}
}
};
return future;
}
}
static class ExecutorThread<V> extends Thread {
private V result;
private Exception exception;
boolean isDone = false;
private Callable<V> callable;
private Object lock;
public ExecutorThread(Callable<V> callable, Object lock) {
this.callable = callable;
this.lock = lock;
}
@Override
public void run() {
try {
result = callable.call();
} catch (Exception e) {
exception = e;
} finally {
synchronized (lock) {
isDone = true;
lock.notifyAll();
}
}
}
public V getResult() {
return result;
}
public Exception getException() {
return exception;
}
public boolean isDone() {
return isDone;
}
}
public static void main(String[] args) throws Exception {
MyExecutor executor = new MyExecutor();
Callable<String> callable = new Callable<String>() {
@Override
public String call() throws Exception {
Thread.sleep(2000);
return "hello MyExecutor!";
}
};
MyFuture<String> future = executor.submit(callable);
// 獲取異步調(diào)取結(jié)果
String result = future.get();
System.out.println("result = " + result);
}
}
集合點(diǎn)
各個線程先是分頭行動没陡,各自到達(dá)一個集合點(diǎn),在集合點(diǎn)需要集齊所有線程索赏,交換數(shù)據(jù)盼玄,然后再進(jìn)行下一步動作。
線程中斷
stop 方法看上去就可以停止線程潜腻,但這個方法被標(biāo)記為了過時埃儿,簡單地說,我們不應(yīng)該使用它融涣,可以忽略它童番。
在 Java 中,停止一個線程的主要機(jī)制是中斷威鹿,中斷并不是強(qiáng)迫終止一個線程剃斧,它是一種協(xié)作機(jī)制,是給線程傳遞一個取消信號专普,但是由線程來決定如何以及何時退出悯衬。
void interrupt()方法 :中斷線程,例如,當(dāng)線程A運(yùn)行時筋粗,線程B可以調(diào)用線程A的interrupt()方法來設(shè)置線程A的中斷標(biāo)志為 true 并立即返回策橘。設(shè)置標(biāo)志僅僅是設(shè)置標(biāo)志,線程A實(shí)際并沒有被中斷娜亿,它會繼續(xù)往下執(zhí)行丽已。如果線程處于了阻塞狀態(tài)(如線程調(diào)用了thread.sleep、thread.join买决、thread.wait沛婴、1.5中的 condition.await、以及可中斷的通道上的 I/O 操作方法后可進(jìn)入阻塞狀態(tài))督赤,這時候若線程B調(diào)用線程A的interrupt()方法嘁灯,線程A在檢查中斷標(biāo)示時如果發(fā)現(xiàn)中斷標(biāo)示為true,則會在這些阻塞方法(sleep躲舌、join丑婿、wait、1.5中的condition.await 及可中斷的通道上的 I/O 操作方法)調(diào)用處拋出 InterruptedException 異常没卸。并且在拋出異常后立即將線程的中斷標(biāo)示位清除羹奉,即重新設(shè)置為 false。拋出異常是為了線程從阻塞狀態(tài)醒過來约计,并在結(jié)束線程前讓程序員有足夠的時間來處理中斷請求
boolean isInterrupted()方法:檢測當(dāng)前線程是否被中斷诀拭,如果是返回 true,否則返回 false煤蚌。并不清除中斷標(biāo)志位耕挨。
public boolean isInterrupted() {
return isInterrupted(false);
}
- boolean interrupted()方法:檢測當(dāng)前線程是否被中斷,如果是返回 true铺然,否則返回 false俗孝。與 isInterrupted 不同的是,該方法如果發(fā)現(xiàn)當(dāng)前線程被中斷魄健,則會清除中斷標(biāo)志赋铝,并且該方法是 static 靜態(tài)方法,可以通過 Thread 類直接調(diào)用沽瘦。另外從下面的代碼可以知道革骨,在 interrupted()內(nèi)部是獲取當(dāng)前調(diào)用線程的中斷標(biāo)志而不是調(diào)用 interrupted()方法的實(shí)例對象的中斷標(biāo)志。
public static boolean interrupted() {
return currentThread().isInterrupted(true);
}
線程對中斷的反應(yīng)
interrupt()對線程的影響與線程的狀態(tài)和在進(jìn)行的IO操作有關(guān)析恋。我們主要考慮線程的狀態(tài)良哲,IO操作的影響和具體IO以及操作系統(tǒng)有關(guān),我們就不討論了助隧。線程狀態(tài)有:
? RUNNABLE:線程在運(yùn)行或具備運(yùn)行條件只是在等待操作系統(tǒng)調(diào)度筑凫。
? WAITING/TIMED_WAITING:線程在等待某個條件或超時。
? BLOCKED:線程在等待鎖,試圖進(jìn)入同步塊巍实。
? NEW/TERMINATED:線程還未啟動或已結(jié)束滓技。
RUNNABLE:如果線程在運(yùn)行中,且沒有執(zhí)行IO操作棚潦,interrupt()只是會設(shè)置線程的中斷標(biāo)志位令漂,沒有任何其他作用。
WAITING/TIMED_WAITING:線程調(diào)用join/wait/sleep方法會進(jìn)入 WAITING 或 TIMED_WAITING狀態(tài)丸边,在這些狀態(tài)時叠必,對線程對象調(diào)用interrupt()會使得該線程拋出InterruptedException。需要注意的是妹窖,拋出異常后纬朝,中斷標(biāo)志位會被清空,而不是被設(shè)置嘱吗。
捕獲到 InterruptedException玄组,通常表示希望結(jié)束該線程滔驾,線程大致有兩種處理方式:
1)向上傳遞該異常谒麦,這使得該方法也變成了一個可中斷的方法,需要調(diào)用者進(jìn)行處理哆致;
2)有些情況绕德,不能向上傳遞異常,比如 Thread 的 run 方法摊阀,它的聲明是固定的耻蛇,不能拋出任何受檢異常,這時胞此,應(yīng)該捕獲異常臣咖,進(jìn)行合適的清理操作,清理后漱牵,一般應(yīng)該調(diào)用 Thread 的 interrupt 方法設(shè)置中斷標(biāo)志位夺蛇,使得其他代碼有辦法知道它發(fā)生了中斷。
BLOCKED:如果線程在等待鎖酣胀,對線程對象調(diào)用interrupt()只是會設(shè)置線程的中斷標(biāo)志位刁赦,線程依然會處于BLOCKED狀態(tài),也就是說闻镶,interrupt()并不能使一個在等待鎖的線程真正“中斷”甚脉。
NEW/TERMINATED:如果線程尚未啟動(NEW),或者已經(jīng)結(jié)束(TERMINATED)铆农,則調(diào)用interrupt()對它沒有任何效果牺氨,中斷標(biāo)志位也不會被設(shè)置。