前文從任務(wù)到線程:Java結(jié)構(gòu)化并發(fā)應(yīng)用程序中介紹了如何安排任務(wù)啟動(dòng)線程。
線程在啟動(dòng)之后,正常的情況下會(huì)運(yùn)行到任務(wù)完成,但是有的情況下會(huì)需要提前結(jié)束任務(wù),如用戶取消操作等。可是,讓線程安全、快速和可靠地停止并不是件容易的事情,因?yàn)镴ava中沒有提供安全的機(jī)制來終止線程。雖然有Thread.stop/suspend等方法,但是這些方法存在缺陷,不能保證線程中共享數(shù)據(jù)的一致性,所以應(yīng)該避免直接調(diào)用。
線程在終止的過程中,應(yīng)該先進(jìn)行操作來清除當(dāng)前的任務(wù),保持共享數(shù)據(jù)的一致性,然后再停止。
慶幸的是,Java中提供了中斷機(jī)制,來讓多線程之間相互協(xié)作,由一個(gè)進(jìn)程來安全地終止另一個(gè)進(jìn)程。
1. 任務(wù)的取消
如果外部的代碼能在某個(gè)操作正常完成之前將其設(shè)置為完成狀態(tài),則該操作為可取消的(Cancellable)。
操作被取消的原因有很多,比如超時(shí),異常,請(qǐng)求被取消等等。
一個(gè)可取消的任務(wù)要求必須設(shè)置取消策略,即如何取消,何時(shí)檢查取消命令,以及接收到取消命令之后如何處理。
最簡(jiǎn)單的取消辦法就是利用取消標(biāo)志位,如下所示:
public class PrimeGenerator implements Runnable {
private static ExecutorService exec = Executors.newCachedThreadPool();
private final List<BigInteger> primes
= new ArrayList<BigInteger>();
//取消標(biāo)志位
private volatile boolean cancelled;
public void run() {
BigInteger p = BigInteger.ONE;
//每次在生成下一個(gè)素?cái)?shù)時(shí)堅(jiān)持是否取消
//如果取消,則退出
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();
}
}
這段代碼用于生成素?cái)?shù),并在任務(wù)運(yùn)行一秒鐘之后終止。其取消策略為:通過改變?nèi)∠麡?biāo)志位取消任務(wù),任務(wù)在每次生成下一隨機(jī)素?cái)?shù)之前檢查任務(wù)是否被取消,被取消后任務(wù)將退出。
然而,該機(jī)制的最大的問題就是無法應(yīng)用于擁塞方法。假設(shè)在循環(huán)中調(diào)用了擁塞方法,任務(wù)可能因擁塞而永遠(yuǎn)不會(huì)去檢查取消標(biāo)志位,甚至?xí)斐捎肋h(yuǎn)不能停止。
1.1 中斷
為了解決擁塞方法帶來的問題,就需要使用中斷機(jī)制來取消任務(wù)。
雖然在Java規(guī)范中,線程的取消和中斷沒有必然聯(lián)系,但是在實(shí)踐中發(fā)現(xiàn):中斷是取消線程的最合理的方式。
Thread類中和中斷相關(guān)的方法如下:
public class Thread {
// 中斷當(dāng)前線程
public void interrupt();
// 判斷當(dāng)前線程是否被中斷
public boolen isInterrupt();
// 清除當(dāng)前線程的中斷狀態(tài),并返回之前的值
public static boolen interrupt();
}
調(diào)用Interrupt方法并不是意味著要立刻停止目標(biāo)線程,而只是傳遞請(qǐng)求中斷的消息。所以對(duì)于中斷操作的正確理解為:正在運(yùn)行的線程收到中斷請(qǐng)求之后,在下一個(gè)合適的時(shí)刻中斷自己。
使用中斷方法改進(jìn)素?cái)?shù)生成類如下:
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;
//使用中斷的方式來取消任務(wù)
while (!Thread.currentThread().isInterrupted())
//put方法會(huì)隱式檢查并響應(yīng)中斷
queue.put(p = p.nextProbablePrime());
} catch (InterruptedException consumed) {
/* 允許任務(wù)退出 */
}
}
public void cancel() {
interrupt();
}
}
代碼中有兩次檢查中斷請(qǐng)求:
- 第一次是在循環(huán)開始前,顯示檢查中斷請(qǐng)求;
- 第二次是在put方法,該方法為擁塞的,會(huì)隱式堅(jiān)持當(dāng)前線程是否被中斷;
1.2 中斷策略
和取消策略類似,可以被中斷的任務(wù)也需要有中斷策略:
即如何中斷,合適檢查中斷請(qǐng)求,以及接收到中斷請(qǐng)求之后如何處理。
由于每個(gè)線程擁有各自的中斷策略,因此除非清楚中斷對(duì)目標(biāo)線程的含義,否者不要中斷該線程。
正是由于以上原因,大多數(shù)擁塞的庫(kù)函數(shù)在檢測(cè)到中斷都是拋出中斷異常(InterruptedException)作為中斷響應(yīng),讓線程的所有者去處理,而不是去真的中斷當(dāng)前線程。
雖然有人質(zhì)疑Java沒有提供搶占式的中斷機(jī)制,但是開發(fā)人員通過處理中斷異常的方法,可以定制更為靈活的中斷策略,從而在響應(yīng)性和健壯性之間做出合理的平衡。
一般情況的中斷響應(yīng)方法為:
- 傳遞異常:收到中斷異常之后,直接將該異常拋出;
- 回復(fù)中斷狀態(tài):即再次調(diào)用Interrupt方法,恢復(fù)中斷狀態(tài),讓調(diào)用堆棧的上層能看到中斷狀態(tài)進(jìn)而處理它。
切記,只有實(shí)現(xiàn)了線程中斷策略的代碼才能屏蔽中斷請(qǐng)求,在常規(guī)的任務(wù)和庫(kù)代碼中都不應(yīng)該屏蔽中斷請(qǐng)求。中斷請(qǐng)求是線程中斷和取消的基礎(chǔ)。
1.3 定時(shí)運(yùn)行
定時(shí)運(yùn)行一個(gè)任務(wù)是很常見的場(chǎng)景,很多問題是很費(fèi)時(shí)間的,就需在規(guī)定時(shí)間內(nèi)完成,如果沒有完成則取消任務(wù)。
以下代碼就是一個(gè)定時(shí)執(zhí)行任務(wù)的實(shí)例:
public class TimedRun1 {
private static final ScheduledExecutorService cancelExec = Executors.newScheduledThreadPool(1);
public static void timedRun(Runnable r,
long timeout, TimeUnit unit) {
final Thread taskThread = Thread.currentThread();
cancelExec.schedule(new Runnable() {
public void run() {
// 中斷線程,
// 違規(guī),不能在不知道中斷策略的前提下調(diào)用中斷,
// 該方法可能被任意線程調(diào)用。
taskThread.interrupt();
}
}, timeout, unit);
r.run();
}
}
很可惜,這是反面的例子,因?yàn)?em>timedRun方法在不知道Runnable對(duì)象的中斷策略的情況下,就中斷該任務(wù),這樣會(huì)承擔(dān)很大的風(fēng)險(xiǎn)。而且如果Runnable對(duì)象不支持中斷, 則該定時(shí)模型就會(huì)失效。
為了解決上述問題,就需要執(zhí)行任務(wù)都線程有自己的中斷策略,如下:
public class LaunderThrowable {
public static RuntimeException launderThrowable(Throwable t) {
if (t instanceof RuntimeException)
return (RuntimeException) t;
else if (t instanceof Error)
throw (Error) t;
else
throw new IllegalStateException("Not unchecked", t);
}
}
public class TimedRun2 {
private static final ScheduledExecutorService cancelExec = newScheduledThreadPool(1);
public static void timedRun(final Runnable r,
long timeout, TimeUnit unit)
throws InterruptedException {
class RethrowableTask implements Runnable {
private volatile Throwable t;
public void run() {
try {
r.run();
} catch (Throwable t) {
//中斷策略,保存當(dāng)前拋出的異常,退出
this.t = t;
}
}
// 再次拋出異常
void rethrow() {
if (t != null)
throw launderThrowable(t);
}
}
RethrowableTask task = new RethrowableTask();
final Thread taskThread = new Thread(task);
//開啟任務(wù)子線程
taskThread.start();
//定時(shí)中斷任務(wù)子線程
cancelExec.schedule(new Runnable() {
public void run() {
taskThread.interrupt();
}
}, timeout, unit);
//限時(shí)等待任務(wù)子線程執(zhí)行完畢
taskThread.join(unit.toMillis(timeout));
//嘗試拋出task在執(zhí)行中拋出到異常
task.rethrow();
}
}
無論Runnable對(duì)象是否支持中斷,RethrowableTask對(duì)象都會(huì)記錄下來發(fā)生的異常信息并結(jié)束任務(wù),并將該異常再次拋出。
1.4 通過Future取消任務(wù)
Future用來管理任務(wù)的生命周期,自然也可以來取消任務(wù),調(diào)用Future.cancel方法就是用中斷請(qǐng)求結(jié)束任務(wù)并退出,這也是Executor的默認(rèn)中斷策略。
用Future實(shí)現(xiàn)定時(shí)任務(wù)的代碼如下:
public class TimedRun {
private static final ExecutorService taskExec = Executors.newCachedThreadPool();
public static void timedRun(Runnable r,
long timeout, TimeUnit unit)
throws InterruptedException {
Future<?> task = taskExec.submit(r);
try {
task.get(timeout, unit);
} catch (TimeoutException e) {
// 因超時(shí)而取消任務(wù)
} catch (ExecutionException e) {
// 任務(wù)異常,重新拋出異常信息
throw launderThrowable(e.getCause());
} finally {
// 如果該任務(wù)已經(jīng)完成,將沒有影響
// 如果任務(wù)正在運(yùn)行,將因?yàn)橹袛喽蝗∠? task.cancel(true); // interrupt if running
}
}
}
1.5 不可中斷的擁塞
一些的方法的擁塞是不能響應(yīng)中斷請(qǐng)求的,這類操作以I/O操作居多,但是可以讓其拋出類似的異常,來停止任務(wù):
- Socket I/O: 關(guān)閉底層socket,所有因執(zhí)行讀寫操作而擁塞的線程會(huì)拋出SocketException;
- 同步 I/O:大部分Channel都實(shí)現(xiàn)了InterruptiableChannel接口,可以響應(yīng)中斷請(qǐng)求,拋出異常ClosedByInterruptException;
- Selector的異步 I/O:Selector執(zhí)行select方法之后,再執(zhí)行close和wakeUp方法就會(huì)拋出異常ClosedSelectorException。
以套接字為例,其利用關(guān)閉socket對(duì)象來響應(yīng)異常的實(shí)例如下:
public class ReaderThread extends Thread {
private static final int BUFSZ = 512;
private final Socket socket;
private final InputStream in;
public ReaderThread(Socket socket) throws IOException {
this.socket = socket;
this.in = socket.getInputStream();
}
public void interrupt() {
try {
// 關(guān)閉套接字
// 此時(shí)in.read會(huì)拋出異常
socket.close();
} catch (IOException ignored) {
} finally {
// 正常的中斷
super.interrupt();
}
}
public void run() {
try {
byte[] buf = new byte[BUFSZ];
while (true) {
int count = in.read(buf);
if (count < 0)
break;
else if (count > 0)
processBuffer(buf, count);
}
} catch (IOException e) {
// 如果socket關(guān)閉,in.read方法將會(huì)拋出異常
// 借此機(jī)會(huì),響應(yīng)中斷,線程退出
}
}
public void processBuffer(byte[] buf, int count) {
}
}
2. 停止基于線程的服務(wù)
一個(gè)應(yīng)用程序是由多個(gè)服務(wù)構(gòu)成的,而每個(gè)服務(wù)會(huì)擁有多個(gè)線程為其工作。當(dāng)應(yīng)用程序關(guān)閉服務(wù)時(shí),由服務(wù)來關(guān)閉其所擁有的線程。服務(wù)為了便于管理自己所擁有的線程,應(yīng)該提供生命周期方來關(guān)閉這些線程。對(duì)于ExecutorService,其包含線程池,是其下屬線程的擁有者,所提供的生命周期方法就是shutdown和shutdownNow方法。
如果服務(wù)的生命周期大于所創(chuàng)建線程的生命周期,服務(wù)就應(yīng)該提供生命周期方法來管理線程。
2.1 強(qiáng)行關(guān)閉和平緩關(guān)閉
我們以日志服務(wù)為例,來說明兩種關(guān)閉方式的不同。首先,如下代碼是不支持關(guān)閉的日志服務(wù),其采用多生產(chǎn)者-單消費(fèi)者模式,生產(chǎn)者將日志消息放入擁塞隊(duì)列中,消費(fèi)者從隊(duì)列中取出日志打印出來。
public class LogWriter {
// 擁塞隊(duì)列作為緩存區(qū)
private final BlockingQueue<String> queue;
// 日志線程
private final LoggerThread logger;
// 隊(duì)列大小
private static final int CAPACITY = 1000;
public LogWriter(Writer writer) {
this.queue = new LinkedBlockingQueue<String>(CAPACITY);
this.logger = new LoggerThread(writer);
}
public void start() {
logger.start();
}
public void log(String msg) throws InterruptedException {
queue.put(msg);
}
private class LoggerThread extends Thread {
//線程安全的字節(jié)流
private final PrintWriter writer;
public LoggerThread(Writer writer) {
this.writer = new PrintWriter(writer, true); // autoflush
}
public void run() {
try {
while (true)
writer.println(queue.take());
} catch (InterruptedException ignored) {
} finally {
writer.close();
}
}
}
}
如果沒有終止操作,以上任務(wù)將無法停止,從而使得JVM也無法正常退出。但是,讓以上的日志服務(wù)停下來其實(shí)并非難事,因?yàn)閾砣?duì)列的take方法支持響應(yīng)中斷,這樣直接關(guān)閉服務(wù)的方法就是強(qiáng)行關(guān)閉,強(qiáng)行關(guān)閉的方式不會(huì)去處理已經(jīng)提交但還未開始執(zhí)行的任務(wù)。
但是,關(guān)閉日志服務(wù)前,擁塞隊(duì)列中可能還有沒有及時(shí)打印出來的日志消息,所以強(qiáng)行關(guān)閉日志服務(wù)并不合適,需要等隊(duì)列中已經(jīng)存在的消息都打印完畢之后再停止,這就是平緩關(guān)閉,也就是在關(guān)閉服務(wù)時(shí)會(huì)等待已提交任務(wù)全部執(zhí)行完畢之后再退出。
除此之外,在取消生產(chǎn)者-消費(fèi)者操作時(shí),還需要同時(shí)告知消費(fèi)者和生產(chǎn)者相關(guān)操作已經(jīng)被取消。
平緩關(guān)閉的日志服務(wù)如下,其采用了類似信號(hào)量的方式記錄隊(duì)列中尚未處理的消息數(shù)量。
public class LogService {
private final BlockingQueue<String> queue;
private final LoggerThread loggerThread;
private final PrintWriter writer;
@GuardedBy("this") private boolean isShutdown;
// 信號(hào)量 用來記錄隊(duì)列中消息的個(gè)數(shù)
@GuardedBy("this") private int reservations;
public LogService(Writer writer) {
this.queue = new LinkedBlockingQueue<String>();
this.loggerThread = new LoggerThread();
this.writer = new PrintWriter(writer);
}
public void start() {
loggerThread.start();
}
public void stop() {
synchronized (this) {
isShutdown = true;
}
loggerThread.interrupt();
}
public void log(String msg) throws InterruptedException {
synchronized (this) {
//同步方法判斷是否關(guān)閉和修改信息量
if (isShutdown) // 如果已關(guān)閉,則不再允許生產(chǎn)者將消息添加到隊(duì)列,會(huì)拋出異常
throw new IllegalStateException(/*...*/);
//如果在工作狀態(tài),信號(hào)量增加
++reservations;
}
// 消息入隊(duì)列;
queue.put(msg);
}
private class LoggerThread extends Thread {
public void run() {
try {
while (true) {
try {
//同步方法讀取關(guān)閉狀態(tài)和信息量
synchronized (LogService.this) {
//如果進(jìn)程被關(guān)閉且隊(duì)列中已經(jīng)沒有消息了,則消費(fèi)者退出
if (isShutdown && reservations == 0)
break;
}
// 取出消息
String msg = queue.take();
// 消費(fèi)消息前,修改信號(hào)量
synchronized (LogService.this) {
--reservations;
}
writer.println(msg);
} catch (InterruptedException e) { /* retry */
}
}
} finally {
writer.close();
}
}
}
}
2.2 關(guān)閉ExecutorService
在ExecutorService中,其提供了shutdown和shutdownNow方法來分別實(shí)現(xiàn)平緩關(guān)閉和強(qiáng)制關(guān)閉:
- shutdownNow:強(qiáng)制關(guān)閉,響應(yīng)速度快,但是會(huì)有風(fēng)險(xiǎn),因?yàn)橛腥蝿?wù)肯執(zhí)行到一半被終止;
- shutdown:平緩關(guān)閉,響應(yīng)速度較慢,會(huì)等到全部已提交的任務(wù)執(zhí)行完畢之后再退出,更為安全。
這里還需要說明下shutdownNow方法的局限性,因?yàn)閺?qiáng)行關(guān)閉直接關(guān)閉線程,所以無法通過常規(guī)的方法獲得哪些任務(wù)還沒有被執(zhí)行。這就會(huì)導(dǎo)致我們無紡知道線程的工作狀態(tài),就需要服務(wù)自身去記錄任務(wù)狀態(tài)。如下為示例代碼:
public class TrackingExecutor extends AbstractExecutorService {
private final ExecutorService exec;
//被取消任務(wù)的隊(duì)列
private final Set<Runnable> tasksCancelledAtShutdown =
Collections.synchronizedSet(new HashSet<Runnable>());
public TrackingExecutor(ExecutorService exec) {
this.exec = exec;
}
public void shutdown() {
exec.shutdown();
}
public List<Runnable> shutdownNow() {
return exec.shutdownNow();
}
public boolean isShutdown() {
return exec.isShutdown();
}
public boolean isTerminated() {
return exec.isTerminated();
}
public boolean awaitTermination(long timeout, TimeUnit unit)
throws InterruptedException {
return exec.awaitTermination(timeout, unit);
}
public List<Runnable> getCancelledTasks() {
if (!exec.isTerminated())
throw new IllegalStateException(/*...*/);
return new ArrayList<Runnable>(tasksCancelledAtShutdown);
}
public void execute(final Runnable runnable) {
exec.execute(new Runnable() {
public void run() {
try {
runnable.run();
} finally {
// 如果當(dāng)前任務(wù)被中斷且執(zhí)行器被關(guān)閉,則將該任務(wù)加入到容器中
if (isShutdown()
&& Thread.currentThread().isInterrupted())
tasksCancelledAtShutdown.add(runnable);
}
}
});
}
}
3. 處理非正常線程終止
導(dǎo)致線程非正常終止的主要原因就是RuntimeException,其表示為不可修復(fù)的錯(cuò)誤。一旦子線程拋出異常,該異常并不會(huì)被父線程捕獲,而是會(huì)直接拋出到控制臺(tái)。所以要認(rèn)真處理線程中的異常,盡量設(shè)計(jì)完備的try-catch-finally代碼塊。
當(dāng)然,異常總是會(huì)發(fā)生的,為了處理能主動(dòng)解決未檢測(cè)異常問題,Thread.API提供了接口UncaughtExceptionHandler。
public interface UncaughtExceptionHandler {
void uncaughtException(Thread t, Throwable e);
}
如果JVM發(fā)現(xiàn)一個(gè)線程因未捕獲異常而退出,就會(huì)把該異常交個(gè)Thread對(duì)象設(shè)置的UncaughtExceptionHandler來處理,如果Thread對(duì)象沒有設(shè)置任何異常處理器,那么默認(rèn)的行為就是上面提到的拋出到控制臺(tái),在System.err中輸出。
Thread對(duì)象通過setUncaughtExceptionHandler方法來設(shè)置UncaughtExceptionHandler,比如這樣:
public class WitchCaughtThread
{
public static void main(String args[])
{
Thread thread = new Thread(new Task());
thread.setUncaughtExceptionHandler(new ExceptionHandler());
thread.start();
}
}
class ExceptionHandler implements UncaughtExceptionHandler
{
@Override
public void uncaughtException(Thread t, Throwable e)
{
System.out.println("==Exception: "+e.getMessage());
}
}
同樣可以為所有的Thread設(shè)置一個(gè)默認(rèn)的UncaughtExceptionHandler,通過調(diào)用Thread.setDefaultUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh)方法,這是Thread的一個(gè)static方法。
下面是一個(gè)例子,即發(fā)生為捕獲異常時(shí)將異常寫入日志:
public class UEHLogger implements Thread.UncaughtExceptionHandler {
// 將未知的錯(cuò)誤計(jì)入到日志中
public void uncaughtException(Thread t, Throwable e) {
Logger logger = Logger.getAnonymousLogger();
logger.log(Level.SEVERE, "Thread terminated with exception: " + t.getName(), e);
}
}
在Executor框架中,需要將異常的捕獲封裝到Runnable或者Callable中并通過execute提交的任務(wù),才能將它拋出的異常交給UncaughtExceptionHandler,而通過submit提交的任務(wù),無論是拋出的未檢測(cè)異常還是已檢查異常,都將被認(rèn)為是任務(wù)返回狀態(tài)的一部分。如果一個(gè)由submit提交的任務(wù)由于拋出了異常而結(jié)束,那么這個(gè)異常將被Future.get封裝在ExecutionException中重新拋出。
public class ExecuteCaught
{
public static void main(String[] args)
{
ExecutorService exec = Executors.newCachedThreadPool();
exec.execute(new ThreadPoolTask());
exec.shutdown();
}
}
class ThreadPoolTask implements Runnable
{
@Override
public void run()
{
Thread.currentThread().setUncaughtExceptionHandler(new ExceptionHandler());
System.out.println(3/2);
System.out.println(3/0);
System.out.println(3/1);
}
}
擴(kuò)展閱讀: