1、摘要
让线程和任务启动很容易,但是,要安全地关闭它们,就不是那么容易了。
Java中没有提供任何机制来安全地终止线程。
中断(Interruption)只是一种协作机制,然一个线程告诉另一个线程:我想让你停下来。
但是,另一个线程是否会停下来取决于它执行的任务里面是否响应了中断,并且采取了什么中断策略。
一个良好的软件应该能很完善地处理失败、关闭和取消等生命周期过程。
2、任务取消
取消某个操作的原因有很多:
1、用户请求取消,如通过取消按钮
2、有时间限制的操作,超时取消
3、应用程序事件,一个事件发生后要求另一个事件取消。
4、错误,出错取消
5、关闭,平缓关闭(通过关闭机制退出)和立即关闭(崩溃、拔电源)
Java中没有一种安全的抢占式方法来停止线程,只有协作式机制。
一个可取消的任务必须拥有取消策略,该策略中详细规定:
1、如何请求取消该任务
2、任务在何时检查是否有取消操作
3、响应该取消操作时应该执行那些操作
3、取消 - 通过保存取消标记并轮询
通过轮询取消状态来取消有一个致命的问题:
如果循环中执行的动作是阻塞的或需要长时间执行的,取消操作会被延迟,甚至不会发生!
/** * 使用volatile类型来保存取消状态, * 并轮询该状态来决定是否要退出 */ @ThreadSafe public class PrimeGenerator implements Runnable { @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); }
//使用示例:运行一秒后取消
List aSecondOfPrimes() throws InterruptedException {
PrimeGenerator generator = new PrimeGenerator();
new Thread(generator).start();
try {
SECONDS.sleep(1);
} finally {
generator.cancel();
}
return generator.get();
}
/** * 残缺的例子 * 生产者将货物放在一个阻塞队列中,消费者从该阻塞队列中取出货物 * 消费者在不需要货物时,将关闭生产者 * 但是,如果生产者的生产速率远大于消费速率,那么阻塞队列会被装满,并阻塞 * 此时生产者就无法检测到取消操作了 */ 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()); //BlockingQueue.put() 是可阻塞的操作 } catch (InterruptedException consumed) { } } public void cancel() { cancelled = true; } } void consumePrimes() throws InterruptedException { BlockingQueue<BigInteger> primes = ...; BrokenPrimeProducer producer = new BrokenPrimeProducer(primes); producer.start(); try { while (needMorePrimes()) consume(primes.take()); } finally { producer.cancel(); } }
4、取消 - 通过中断来取消
在Java语言规范中,没有将中断与任何取消语义关联起来,但是实际上,如果在取消操作之外的操作中使用中断,都不合适,并且很难支撑起大的应用。
每一个线程都有一个boolean类型的中断状态,当中断线程时,该状态将被设为true。
Thread类中包含了中断线程及查询线程中断状态的方法:
interrupt() 方法能够中断目标线程;
isInterrupted() 方法能够返回目标线程的中断状态。
interrupted() 方法能够返回当前线程的中断状态,并情况该状态,这是清除中断状态的唯一方法。
调用interrupted方法时要小心,因为它会清除中断状态。如果调用后返回true,必须决定响应中断还是恢复中断。
千万不要使得中断丢失!
调用interrupt并不是意味着立即停止目标线程的工作,而只是传递了请求中断的消息。
中断的正确理解是:发出中断请求,然后目标线程在下一个合适的时刻中断自己,当然,它也可以忽略该请求。
通常,中断是实现取消的最合理方式。
/** * 残缺生产者的中断实现 */ 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(); } }
5、中断策略
中断策略:
即如何处理中断,规定了线程如何解释某个中断请求。
最合理的中断策略是线程级取消操作或服务级取消操作:
尽快退出,必要时进行清理,告诉所有者该线程已经退出。
任务通常不会拥有自己的线程,而是在某个服务拥有的线程中执行(如线程池),
对于非线程拥有者的代码来说,应该小心地保存中断状态。以便拥有线程的代码对中断做出响应。
这就是大多数可阻塞库函数的做法:
抛出InterruptedException作为中断响应。
因为它们永远不会在自己拥有的线程中运行,
因此,尽快退出执行流程,并把中断消息传递给调用者,
从而使得调用栈的上层代码能够采取进一步操作。
检测到中断请求时,任务并不是必须放弃所有操作,它们可以推迟处理中断请求。
除非我们明确知道中断请求的中断策略,我们的任务代码不能对线程中的中断策略有任何地猜测。
由于每个线程拥有各自的中断策略,因此,除非知道中断对于该线程的含义,否则就不应该中断该线程。
6、响应中断
当调用可中断的阻塞方法时,有两种实用策略来处理中断异常:
1、恢复中断状态,从而使上层代码能够对其进行处理。
这在某些不想或不能抛出异常的地方有用,如使用Runnable来定义的任务。
2、传递中断异常。从而使自己的代码也成为可中断的阻塞方法。
只有实现了线程中断策略的代码才能屏蔽中断请求。在常规的任务和库代码中都不应该这么做。
/** * 在不可取消的但调用了可中断的阻塞方法(会抛出InterruptException)的任务中,必须在循环中调用可中断的阻塞方法, * 并在发现中断后重新尝试恢复之前的运行 * 并且,应该在本地保存中断状态,并在返回前恢复中断状态(在捕获时恢复可能会引发无限循环,不断地恢复并捕获) */ public Task getNextTask(BlockingQueue<Taskgt; queue) { boolean interrupted = false; try { while (true) { try { return queue.take(); } catch (InterruptedException e) { interrupted = true; // fall through and retry 重新尝试 } } } finally { if (interrupted) Thread.currentThread().interrupt(); } }
/** * 示例:计时运行-在外部线程中安排中断 * 在当前线程中运行任务 * 然后在ScheduledExecutorService中计时对当前线程发中断请求 * 有一个致命缺陷:当前线程必须能够响应中断,并在中断时退出。 * * 在中断线程时,必须了解它的中断策略 */ private static final ScheduledExecutorService cancelExec = ...; public static void timedRun(Runnable r, long timeout, TimeUnit unit) { final Thread taskThread = Thread.currentThread(); //在ScheduledExecutorService中计时中断当前线程 cancelExec.schedule( new Runnable() { public void run() { taskThread.interrupt(); } }, timeout, unit); //在当前线程中运行任务 r.run(); }
/** * 示例:计时运行-在专门的线程中中断任务 * 任务在专门的,我们自己定义了中断策略的线程中运行 * 在ScheduledExecutorService中计时中断专门的线程 */ 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) { this.t = t; } } void rethrow() { //重新抛出捕获到的异常 if (t != null) throw launderThrowable(t); } } RethrowableTask task = new RethrowableTask(); final Thread taskThread = new Thread(task); taskThread.start(); cancelExec.schedule( new Runnable() { public void run() { taskThread.interrupt(); } }, timeout, unit); taskThread.join(unit.toMillis(timeout)); task.rethrow(); //重新抛出捕获到的异常 }
7、使用Future来实现取消
通过ExecutorService.submit 方法提交的任务将返回一个Future来描述任务。
Future方法有cancel方法来取消任务,该方法带有一个boolean类型的mayInterruptIfRunning参数:
参数为true:任务能够接收中断,能够在运行任务时中断线程。
参数为false:如果任务还没有运行,那么就取消它。适合于不处理中断的任务。
但是,除非我们了解线程的中断策略,否则都不该中断线程。
那么什么时候mayInterruptIfRunning可以为true呢?
执行任务的线程是由标准的Executor创建的,因为Executor的中断策略是中断则退出。
在尝试取消任务时,不应该通过关闭线程池,因为线程池可能正在运行着多个任务。
只能通过Future来取消线程池中的任务,正因为如此,在编写任务时,将中断视为一个取消请求。
/** * 通过Future来实现取消 */ 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) { // task will be cancelled below } catch (ExecutionException e) { // exception thrown in task; rethrow throw launderThrowable(e.getCause()); } finally { // Harmless if task already completed task.cancel(true); // interrupt if running } }
当Future.get方法抛出InterruptedException或TimeoutException时,如果此时不需要结果,就可以取消任务。
8、处理不可中断的阻塞
并非所有的阻塞方法都可以响应中断。c此时。怎么关闭它们?
1、Java.io包中的同步Socket I/O
关闭底层套接字,其上的阻塞方法将抛出SocketException。
2、Java.io包中的同步I/O
中断在一个InterruptibleChannel上等待的线程时,将抛出ClosedByInterruptException异常并关闭链路。
导致在链路上等待的线程都抛出AsynchronousCloseException。
3、Selector异步IO
如果线程在调用Selector.select方法时阻塞,调用close或wakeup方法会使得线程抛出ClosedSelectorException异常,并提前返回。
4、获取某个锁
Lock类提供了lockInterruptibly方法,使得能够等待锁时还能响应中断。
/** * 通过改写interrupt方法将非标准的取消操作封装在Thread中 * 即对于Socket IO上的阻塞,关闭底层Socket来实现取消 */ public class ReaderThread extends Thread { 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 { socket.close(); //关闭底层socket来抛出异常,从而能够检测异常并退出 } 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) { /* Allow thread to exit */ } } }
/** * 既然可以通过改写Thread的interrupt方法来实现非标准的取消操作, * 同样,我们也可以改写Future的cancel方法来实现非标准取消操作。 * 因为interrupt方法和cancel方法都是通过向线程发消息来实现关闭指令的。 * newTaskFor是一个工厂方法,它将创建Future来表示任务。它能返回一个RunnableFuture接口 */ public interface CancellableTask<T> extends Callable<T> { void cancel(); RunnableFuture<T> newTask(); } @ThreadSafe public class CancellingExecutor extends ThreadPoolExecutor { ... protected<T> RunnableFuture<T> newTaskFor(Callable<T> callable) { if (callable instanceof CancellableTask) return ((CancellableTask<T>) callable).newTask(); else return super.newTaskFor(callable); } } public abstract class SocketUsingTask<T> implements CancellableTask<T> { @GuardedBy("this") private Socket socket; protected synchronized void setSocket(Socket s) { socket = s; } //cancel被改写 public synchronized void cancel() { try { if (socket != null) socket.close(); } catch (IOException ignored) { } } public RunnableFuture<T> newTask() { //这里返回了重写了cancel的Future return new FutureTask<T>(this) { public boolean cancel(boolean mayInterruptIfRunning) { try { SocketUsingTask.this.cancel(); } finally { return super.cancel(mayInterruptIfRunning); } } }; } }
9、停止基于线程的服务
一个服务通常会拥有多个线程(例如线程池),怎么正确地停止该服务呢?
正确地封装原则是:除非拥有某个线程,否则不能对该线程进行操控。
对于持有线程的服务,只要服务存在的时间大于创建线程的方法存在的时间,就应该提供控制其生命周期的方法。
/** * 没有提供关闭方法的Logger服务 * 日志线程可以被停止(即抛出异常时) * 但是日志服务却没有正确地被关闭 * 直接关闭(只是使日志线程退出),可能会丢失日志记录;还可能会使其他线程调用log时阻塞,因为队列此时可能是满的。。。 */ public class LogWriter { private final BlockingQueue<String> queue; private final LoggerThread logger; 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 { private final PrintWriter writer; ... public void run() { try { while (true) writer.println(queue.take()); } catch(InterruptedException ignored) { } finally { writer.close(); } } } }
/** * 正确地关闭方法:在关闭时,将队列中保存的消息都记录到日志 * 采用reservations记录未写入的消息的数量 * 采用isShutDown保存关闭请求,在关闭请求为true时,停止接收日志
* 两个记录信息都要同步地被访问,因为日志服务可能被多个线程使用 * 关闭时,将未写入的消息都写入日志 */ public class LogService { private final BlockingQueue<String> queue; private final LoggerThread loggerThread; private final PrintWriter writer; @GuardedBy("this") private boolean isShutdown; @GuardedBy("this") private int reservations; public void start() { loggerThread.start(); } public void stop() { synchronized (this) { isShutdown = true; } loggerThread.interrupt(); } public void log(String msg) throws InterruptedException { synchronized (this) { if (isShutdown) throw new IllegalStateException(...); ++reservations; } queue.put(msg); } private class LoggerThread extends Thread { public void run() { try { while (true) { try { synchronized (this) { if (isShutdown && reservations == 0) break; } String msg = queue.take(); synchronized (this) { --reservations; } writer.println(msg); } catch (InterruptedException e) { /* retry */ } } } finally { writer.close(); } } } }
10、停止基于线程的服务- ExecutorService
shutdown和shutdownNow。
11、“毒丸” 对象
得到毒丸时,立即停止。
有一个缺陷,必须知道有多少个待停止的线程(要放多少个毒丸)。
public class IndexingService { private static final File POISON = new File(""); private final IndexerThread consumer = new IndexerThread(); private final CrawlerThread producer = new CrawlerThread(); private final BlockingQueue<File> queue; private final FileFilter fileFilter; private final File root; class CrawlerThread extends Thread { /* Listing 7.18 */ } class IndexerThread extends Thread { /* Listing 7.19 */ } public void start() { producer.start(); consumer.start(); } public void stop() { producer.interrupt(); } public void awaitTermination() throws InterruptedException { consumer.join(); } public class CrawlerThread extends Thread { public void run() { try { crawl(root); } catch (InterruptedException e) { /* fall through */ } finally { //退出,放毒丸 while (true) { try { queue.put(POISON); break; } catch (InterruptedException e1) { /* retry */ } } } } private void crawl(File root) throws InterruptedException { ... } } public class IndexerThread extends Thread { public void run() { try { while (true) { File file = queue.take(); if (file == POISON) //吃到毒丸,狗带 break; else indexFile(file); } } catch (InterruptedException consumed) { } } }
12、shutdownNow的局限性
shutdownNow关闭时,会返回所有已提交但是未开始的任务,但是却会丢弃已经开始执行但未结束的任务。
可以在任务执行时,跟踪任务的执行过程,如果未执行完毕就关闭,就将它记录起来。
以此来保留已提交但未结束的任务。
/** * 用一个同步的set记录已提交但未完成的任务 */ public class TrackingExecutor extends AbstractExecutorService { private final ExecutorService exec; private final Set<Runnable> tasksCancelledAtShutdown = Collections.synchronizedSet(new HashSet<Runnable>()); ... 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 { if (isShutdown() && Thread.currentThread().isInterrupted()) //服务被关闭,但是任务没有结束 tasksCancelledAtShutdown.add(runnable); //加入set } } }); } // delegate other ExecutorService methods to exec }
/** * 网页爬虫为例,演示TrackingExecutorService的使用 * 关闭时,保存未爬取和未爬取结束的网页url 以便下次爬取时使用 */ public abstract class WebCrawler { private volatile TrackingExecutor exec; @GuardedBy("this") private final Set<URL> urlsToCrawl = new HashSet<URL>(); ... public synchronized void start() { exec = new TrackingExecutor(Executors.newCachedThreadPool()); for (URL url : urlsToCrawl) submitCrawlTask(url); //每一个网页的爬取使一个任务 urlsToCrawl.clear(); } public synchronized void stop() throws InterruptedException { try { saveUncrawled(exec.shutdownNow()); //保存未开始的 if (exec.awaitTermination(TIMEOUT, UNIT)) saveUncrawled(exec.getCancelledTasks()); //保存未结束的 } finally { exec = null; } } protected abstract List<URL> processPage(URL url); private void saveUncrawled(List<Runnable> uncrawled) { for (Runnable task : uncrawled) urlsToCrawl.add(((CrawlTask) task).getPage()); } private void submitCrawlTask(URL u) { exec.execute(new CrawlTask(u)); } private class CrawlTask implements Runnable { private final URL url; ... public void run() { for (URL link : processPage(url)) { if (Thread.currentThread().isInterrupted()) //使用interrupt作为结束消息 return; submitCrawlTask(link); } } public URL getPage() { return url; } } }
13、处理非正常的线程终止
程序可能会由于各种原因异常结束,异常结束时,应该通知它的上层调用者。
/** * 典型的线程池工作者线程结构 * 捕获到异常时在本地保存异常,最后通知框架 */ public void run() { Throwable thrown = null; try { while (!isInterrupted()) runTask(getTaskFromWorkQueue()); } catch (Throwable e) { thrown = e; } finally { threadExited(this, thrown); } }
Thread API中,提供了UncaughtExceptionHandler来处理线程由于未捕获的异常而终结的情况。
如果没有注册异常处理器,默认行为是将栈信息打印到System.err。
我们可以定义自己的处理器(通过继承Thread.UncaughtExceptionHandler),
并将其注册给线程(通过setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh))
/** * 发生未捕获异常时,将其信息记录到日志 */ public class UEHLogger implements Thread.UncaughtExceptionHandler { public void uncaughtException(Thread t, Throwable e) { Logger logger = Logger.getAnonymousLogger(); logger.log(Level.SEVERE, "Thread terminated with exception: " + t.getName(), e); } }
在运行较长时间的应用程序中,通常会为所有线程指定一个未捕获异常处理器,该处理器至少都会将异常信息记录到日志中。
要为线程池中所有的线程绑定同一个异常处理器,就要为线程池提供一个ThreadFactory,
在该ThreadFactory中,为线程绑定异常处理器。
14、JVM关闭
关闭钩子:
在正常关闭中,JVM首先调用所有已注册的关闭钩子。
ShutdownHook是在Runtime.addShutdownHook中注册的但未立即启动的线程。
JVM不保证关闭钩子的启动顺序。
如果任何应用程序线程(守护进程或非守护进程)在关闭时仍在运行,则它们将继续与关闭进程并发运行。
当所有关闭钩子都完成时,如果runFinalizersOnExit为true,JVM可能会选择运行终结器,然后停止。
JVM不会试图停止或中断在关机时仍在运行的任何应用程序线程;当JVM最终停止时,它们会突然终止。
关机钩子可用于服务或应用程序清理,例如删除临时文件或清理操作系统未自动清理的资源。
/** * 注册一个关闭钩子来停止日志服务 */ public void start() { Runtime.getRuntime().addShutdownHook( new Thread() { public void run() { try { LogService.this.stop(); } catch (InterruptedException ignored) {} } }); }
守护线程:
普通线程与守护线程之间的差异仅在于退出时发生的行为。
当JVM退出时,任何守护线程都会被立即抛弃,甚至finally代码块也不执行。
守护线程通常不能用于管理服务的生命周期,很少有服务满足能够被突然终止的要求。
终结器:
finalizer
能不用尽量不用。