Java多執行緒之執行緒中斷
取消任務的方式
Java中沒有提供任何機制來安全地終止執行緒,但是提供了中斷(Interruption)協作機制,能夠使一個執行緒終止另一個執行緒的當前工作. 一般取消或停止某個任務,很少採用立即停止,因為立即停止會使得共享資料結構出於不一致的狀態.這也是Thread.stop(),Thread.suspend()以及Thread.resume()不安全的原因而廢棄.
Java中有三種方式可以終止當前執行的執行緒:
- 設定某個"已請求取消(Cancellation Requested)"標記,而任務將定期檢視該標記的協作機制來中斷執行緒.
- 使用Thread.stop()強制終止執行緒,但是因為這個方法”解鎖”導致共享資料結構處於不一致而不安全被廢棄.
- 使用Interruption中斷機制.
使用中斷標記來中斷執行緒.
public class PrimeGenerator implements Runnable {
private static ExecutorService exec = Executors.newCachedThreadPool();
@GuardedBy("this") private final List<BigInteger> primes
= new ArrayList<BigInteger>();
private volatile boolean cancelled;
public void run() {
BigInteger p = BigInteger.ONE;
while (!cancelled) {
p = p.nextProbablePrime();
synchronized (this) {
primes.add(p);
}
}
}
public void cancel() {
cancelled = true;
}
public synchronized List<BigInteger> get() {
return new ArrayList<BigInteger>(primes);
}
static List<BigInteger> aSecondOfPrimes() throws InterruptedException {
PrimeGenerator generator = new PrimeGenerator();
exec.execute(generator);
try {
SECONDS.sleep(1);
} finally {
generator.cancel();
}
return generator.get();
}
}
設定標記的中斷策略: PrimeGenerator使用一種簡單取消策略,客戶端程式碼通過調研cancel來請求取消,PrimeGenerator在每次搜尋素數時前先檢查是否存在取消請求,如果不存在就退出.
但是使用設定標記的中斷策略有一問題: 如果任務呼叫呼叫阻塞的方法,比如BlockingQueue.put,那麼可能任務永遠不會檢查取消標記而不會結束.
class BrokenPrimeProducer extends Thread {
private final BlockingQueue<BigInteger> queue;
private volatile boolean cancelled = false;
BrokenPrimeProducer(BlockingQueue<BigInteger> queue) {
this.queue = queue;
}
public void run() {
try {
BigInteger p = BigInteger.ONE;
while (!cancelled)
//此處阻塞,可能永遠無法檢測到結束的標記
queue.put(p = p.nextProbablePrime());
} catch (InterruptedException consumed) {
}
}
public void cancel() {
cancelled = true;
}
}
解決辦法也很簡單: 使用中斷而不是使用boolean標記來請求取消
使用中斷(Interruption)請求取消
Thread類中的中斷方法:
public void interrupt()
請求中斷,設定中斷標記,而並不是真正中斷一個正在執行的執行緒,只是發出了一個請求中斷的請求,由執行緒在合適的時候中斷自己.
public static native boolean interrupted()
;判斷執行緒是否中斷,會擦除中斷標記(判斷的是當前執行的執行緒),另外若呼叫Thread.interrupted()返回為true時,必須要處理,可以丟擲中斷異常或者再次呼叫interrupt()來恢復中斷.
public native boolean isInterrupted()
;判斷執行緒是否中斷,不會擦除中斷標記
故而上面問題的解決方案如下:
public class PrimeProducer extends Thread {
private final BlockingQueue<BigInteger> queue;
PrimeProducer(BlockingQueue<BigInteger> queue) {
this.queue = queue;
}
public void run() {
try {
BigInteger p = BigInteger.ONE;
while (!Thread.currentThread().isInterrupted())
queue.put(p = p.nextProbablePrime());
} catch (InterruptedException consumed) {
/* Allow thread to exit */
}
}
public void cancel() {
interrupt();
}
}
那麼thread.interrupt()呼叫後到底意味著什麼?
首先一個執行緒不應該由其他執行緒來強制中斷或者停止,而應該由執行緒自己停止,所以Thread.stop(),Thread.suspend(),Thread.resume()都已被廢棄.而{@link Thread#interrupt}作用其實不是中斷執行緒,而請求執行緒中斷.具體來說,當呼叫interrupt()方法時:
- 如果執行緒處於阻塞狀態時(例如處於sleep,wait,join等狀態時)那麼執行緒將立即退出阻塞狀態而丟擲InterruptedException異常.
- 如果 執行緒處於正常活動狀態,那麼會將執行緒的中斷標記設定為true,僅此而已.被設定中斷標記的執行緒將繼續執行而不受影響.
interrupt()並不能真正的中斷執行緒,需要被呼叫的執行緒自己進行配合才行:
- 在正常執行任務時,經常檢查本執行緒的中斷標誌位,如果被設定了中斷標誌就自行停止執行緒。
- 在呼叫阻塞方法時正確處理InterruptedException異常。