前面几章我们一直是创建和开启线程,而有时候我们要结束任务或线程,这并不是很容易的,因为,java 并没有提供任何机制来安全终止线程(在未来的jdk版本中会不会加入呢?)它提供了中断。这是一种机制,能够在一个线程终止另一个线程的工作。
任务取消
一种协作方式是设置某个“已请求取消”标识,而任务将定时查看该标志。下面程序,将持续枚举素数,直到它将被取消。cancle方法设置canceled标志,并且主循环在搜索下一个素数下一个之前,首先检查这个标志(canceled必须为volatile 原因请看
java内存模型)
public class PrimeGenerator implements Runnable {
private List<BigInteger> primes = new ArrayList<BigInteger>();
private volatile boolean canceled;
@Override
public void run() {
BigInteger bigInteger = BigInteger.ONE;
while (!canceled) {
bigInteger = bigInteger.nextProbablePrime();
synchronized (this) {
primes.add(bigInteger);
}
}
}
public void cancel() { this.canceled = true; }
public synchronized List<BigInteger> get() {
return new ArrayList<BigInteger>(primes);
}
}
现在我们要让一个素数生成器运行1秒后停止,(虽然不是很精确,因为每一条代码是有延时的),我们可以用try finally 来完成,以保证线程会停止,否则线程会一直消耗CPU时钟周期,导致JVM退出。
List<BigInteger> aSecondOfPrimes() throws InterruptedException {
PrimeGenerator generator = new PrimeGenerator();
new Thread(generator).start();
try {
Thread.sleep(1000);
} finally {
generator.cancel();
}
return generator.get();
}
中断
PrimeGenerator中的取消机制最终会使得搜索的素数任务退出,但是退出过程中需要花费一定的时间,然而,如果任务调度了一些阻塞方法,(BlockingQueue.put)那么可能产生一个问题——任务可能永远不会检查取消标示,永远不会结束。
为取保线程能退出,我们通常使用中断,当我们调用interrupt,并不意味着立即停止目标线程正在运行的线程,而只是传递了一个请求中断的信息,它会在线程下一个合适的的时刻中断自己。wait、sleep、join、将严格处理这种请求,当他们收到一个中断请求,或饿着开始执行时发现中断状态时,将抛出异常。
使用静态的interrupted时应该小心,因为它会清除当前线程的中断状态,如果返回true,除非你你想屏蔽这个中断,否则必须对它进行处理,抛出异常或者再次调用interrupt来恢复中断状态
try {
...
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
通常,我们用中断来取消任务比检查标记更好,是最合适的取消任务方式,我们看一个更加健壮的获得素数的类。
public class PrimeProducer extends Thread{
private final BlockingQueue<BigInteger> queue;
public PrimeProducer(BlockingQueue<BigInteger> queue) {
this.queue = queue;
}
@Override
public void run() {
try {
BigInteger p = BigInteger.ONE;
while(!Thread.currentThread().isInterrupted()) //①用线程的状态来检查
queue.put(p = p.nextProbablePrime());
} catch (InterruptedException e) {
//中断将线程退出
}
}
public void cancel() { interrupt(); }
}
我们分析下,在while时,我们有两次的检查中断,while中有一次,在执行put的时候有一次,这样我们比用flag标识有更高的效用性,通常,我们也是通过这种方式来取消线程的。
通过Future来实现中断
public static void timeRun(Runnable r ,long timeout, TimeUnit unit) {
Future<?> task = taskExec.submit(r);
try {
task.get(timeout,unit);
} catch (InterruptedException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (ExecutionException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} catch (TimeoutException e) {
// TODO Auto-generated catch block
e.printStackTrace();
} finally {
task.cancel(true);
}
}
处理不可中断的请求
java中并非所有的可阻塞方法或者阻塞机制都能响应中断,如果一个线程由于执行同步的Socket I/O或者等待获得内置锁而阻塞,那么中断请求只能设置线程的中断状态,除此之外,没有任何作用,对于那些由于执行不可中断的操作而被阻塞的线程,可以使用类似于中断手段来停止这些线程,但是要求我们必须知道线程阻塞的原因。
- java.io包中的同步Socket I/O。
- java.io包中的同步I/O
- Select的异步I/O
- 获取某个锁
public class ReadThread extends Thread {
private static final int BUFSZ = 1024;
private final Socket socket;
private final InputStream in;
public ReadThread(Socket socket, InputStream in) {
this.socket = socket;
this.in = in;
}
@Override
public void interrupt() {
try {
socket.close();
} catch (IOException e) {
} finally {
super.interrupt();
}
}
@Override
public void run() {
byte[] buf = new byte[BUFSZ];
int count;
try {
while (true) {
count = in.read(buf);
if (count < 0)
break;
else if (count > 0)
processBuffer(buf, count);
}
} catch (Exception e) {
}
}
}
我们看到 如果只终止ReadThread线程是没用的,socket是不能关闭的,所以我们要重写interrupt方法 将socket关闭。当然还要调用父类的interrupt
采用newTaskFor来封装非标准的取消
我们通过newTaskFor方法进一步优化ReadThread中封装非标准取消的技术,ThreadPoolExecutor是java 6新增功能,当把一个Callable提交给ExecutorService时,submit方法返回一个Future,我们可以通过Future来取消任务。newTaskFor是一个工厂方法,它将创建一个Future来代表任务。newTaskFor还能返回一个RunnableFuture借口,该接口扩展了Future和Runnable。
通过定制便是任务的Future可以改变Future.cancel的行为。
首先我们继承Callable接口进行扩展
interface CancellableTask<T> extends Callable<T> {
void cancel();
RunnableFuture<T> newTask();
}
然后定义一个抽象的 SocketUsingTask继承 CancellableTask,重写 cancel(Callellable接口)方法,实现的socket关闭,重写 newTask为 CancellingExecutor类使用。
public abstract class SocketUsingTask<T> implements CancellableTask<T> {
private Socket socket;
protected synchronized void setSocket(Socket s) {
socket = s;
}
@Override
public synchronized void cancel() {
try {
if (socket != null)
socket.close();
} catch (Exception e) {
}
}
@Override
public RunnableFuture<T> newTask() {
return new FutureTask<T>(this) {
@Override
public boolean cancel(boolean mayInterruptIfRunning) {
try {
SocketUsingTask.this.cancel();
} catch (Exception e) {
} finally {
//不建议将return 写在finally中
}
return super.cancel(mayInterruptIfRunning);
}
};
}
}
现在我们继承 ThreadPoolExecutor,写一个工厂类( CancellingExecutor )
public class CancellingExecutor extends ThreadPoolExecutor {
public CancellingExecutor(int corePoolSize, int maximumPoolSize,
long keepAliveTime, TimeUnit unit, BlockingQueue<Runnable> workQueue) {
super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue);
}
@Override
protected <T> RunnableFuture<T> newTaskFor(Callable<T> callable) {
if (callable instanceof CancellableTask)
return ((CancellableTask<T>) callable).newTask();
else
return super.newTaskFor(callable);
}
}
ok 现在我们测试
public class TTT {
public static void main(String[] args) {
CancellingExecutor cancellingExecutor = new CancellingExecutor(10, 20, 200, TimeUnit.SECONDS, new ArrayBlockingQueue<Runnable>(10));
Future<String> future = cancellingExecutor.submit(new SocketUsingTask<String>() {
@Override
public String call() throws Exception {
return "hello";
}
});
future.cancel(true);
}
}
停止基于线程的服务
对于持有线程的服务,只要服务的存在时间大于创建线程的方法的存在时间,那么就应该提供生命周期方法。
public class LogService {
private final BlockingQueue<String> queue;
private final LoggerThread loggerThread;
private final PrintWriter printWriter;
private volatile boolean isShutdown;
private int reservation;
public LogService(BlockingQueue<String> queue, boolean isShutdown,
PrintWriter printWriter) {
this.queue = queue;
this.loggerThread = new LoggerThread();
this.printWriter = printWriter;
}
public void start() {
loggerThread.start();
}
public void stop() {
synchronized (this) {
isShutdown = true;
}
loggerThread.interrupt();
}
public void put(String msg) throws InterruptedException {
synchronized (this) {
if (isShutdown)
throw new IllegalStateException();
++reservation;
}
queue.put(msg);
}
private class LoggerThread extends Thread {
@Override
public void run() {
try {
while (true) {
try {
synchronized (LogService.this) {
if (isShutdown && reservation == 0)
break;
}
String msg = queue.take();
printWriter.write(msg);
synchronized (LogService.this) {
--reservation;
}
} catch (InterruptedException e) {
e.printStackTrace();
}
}
} finally {
// 接到interrupt会执行
printWriter.close();
}
}
}
}
关闭ExecutorService
ExecutorService提供两个关闭的方法,shutdown 和shutdownNow。shutdownNow首先关闭当前执行线程,然后返回未执行的任务,而shutdown则等待执行完成。两种方法差别在于安全性和响应性。
直接看例子了,这没什么好说的。
public class LogService {
private static final TimeUnit UNIT = null;
private static final long TIMEOUT = 0;
private final ExecutorService exec = newSingleThreadExecutor();
public void start() { }
public void stop() {
try {
exec.shutdown();
exec.awaitTermination(TIMEOUT, UNIT);
} finally {
writer.close();
}
}
public void log(String msg) {
try {
exec.submit(new WriteTask(msg));
} catch (Exception e) { }
}
}
毒丸对象
另一种关闭生产者-消费者服务的方式就是使用“毒丸”对象,其实就是指往对象里面放一个标志对象,当得到这个对象就立即停止,这就需要在执行方法里面判断,消费者读到毒丸后就不会再执行,同样生产者提交毒丸后,就不能再提交任务。
只有生产者和消费者都已知的情况下,才可以使用“毒丸”,当生产者和消费者和数量较大时,方法变的难以使用。
直接看书中的例子,这个不是很常用,粘过来,以备用吧。
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 {
class IndexerThread extends Thread {
public void start() {
producer.start();
consumer.start();
}
/* Listing 7.18 */
} /* Listing 7.19 */
}
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) { }
 }
}
示例:只执行一次的服务
如果某个方法需要处理一批任务,并且当所有任务都处理完成后才返回,那么可以通过一个私有的Executor来简化服务的生命周期,其中该Executor的生命周期由这个方法来控制。
下面程序checkMail方法能在多台主机上并行地检查新邮件,它创建一个私有的Executor,并向每台主机提交一个任务,当所有邮件任务都执行完成后,关闭并结束。
boolean checkMail(Set<String> hosts, long timeout, TimeUnit unit)
throws InterruptedException {
ExecutorService exec = Executors.newCachedThreadPool();
final AtomicBoolean hasNewMail = new AtomicBoolean(false);
try {
for (final String host : hosts) {
exec.execute(new Runnable() {
@Override
public void run() {
if (check(host))
hasNewMail.set(true);
}
});
}
} finally {
exec.shutdown();
exec.awaitTermination(timeout, unit);
}
return hasNewMail.get();
}
用AtomicBoolean代替volatile boolean,是因为从内部的Runnable中访问hasNewMail标志,它必须定义为final
shutdownNow的局限性
当通过shutdownNow来强行关闭ExecutorServices时,它会尝试正在执行的任务,并返回所有已提交但尚未开始的任务。而然我想得到哪些已经开始但是尚未执行完成就被打断的线程,就需要自己封装了。
设计一个TrackingExecutor类继承AbstractExecutorService 重写execute 当执行时被中断记录在集合里。
public class TrackingExecutor extends AbstractExecutorService {
private final ExecutorService exec;
private final Set<Runnable> taskCancellAtShutdown = Collections.synchronizedSet(new HashSet<Runnable>());
public TrackingExecutor(ExecutorService exec) {
this.exec = exec;
}
public List<Runnable> getCancelledTask() {
if(!exec.isTerminated())
throw new IllegalStateException();
return new ArrayList<Runnable>(taskCancellAtShutdown);
}
@Override
public void execute(Runnable command) {
exec.execute(new Runnable() {
@Override
public void run() {
try {
command.run();
} finally {
if (isShutdown() && Thread.currentThread().isInterrupted())
taskCancellAtShutdown.add(command);
}
}
});
}
...
}
public abstract class WebCrawler {
private volatile TrackingExecutor exec;
private final Set<URL> urlToCrawl = new HashSet<URL>();
public synchronized void start() {
exec = new TrackingExecutor(Executors.newCachedThreadPool());
for (URL url : urlToCrawl) submitCrawlTask(url);
urlToCrawl.clear();
}
public synchronized void stop() {
try {
saveUncrawled(exec.shutdownNow());
if(exec.awaitTermination(timeout, unit))
saveUncrawled(exec.getCancelledTask());
}finally {
exec = null;
}
}
protected abstract List<URL> processPage(URL url);
private void saveUncrawled(List<Runnable> uncrawled) {
for (Runnable task : uncrawled) {
urlToCrawl.add(((CrawTask)task).getUrl());
}
}
private void submitCrawlTask(URL u) {
exec.execute(new CrawTask(u));
}
private class CrawTask implements Runnable {
private final URL url;
public CrawTask(URL url) {
this.url = url;
}
@Override
public void run() {
for (URL link : processPage(url)) {
if(Thread.currentThread().isInterrupted())
return;
submitCrawlTask(link);
}
}
public URL getUrl() {
return url;
}
}
}
处理非正常的线程终止
Thread API中提供了一个UncaughtExceptionHandler,能够检测出线程由于未捕获的异常儿终结的情况。
当一个线程由于未捕获异常而终止时,JVM会把这个事件报告给应用程序提供的UncaughtExceptionHandler异常处理器。如果没有提过任何异常处理器,就会输出System.err。
class UELogger implements Thread.UncaughtExceptionHandler {
@Override
public void uncaughtException(Thread t, Throwable e) {
//进行错误日志的捕获
}
}
可以通过Thread.
setUncaughtExceptionHandler
设置
只有通过executor提交的任务抛出的异常交给未捕获异常处理器,通过submit提交的任务,异常都会作为任务返回状态的一部分。如果一个由submit提交的任务由于跑出异常而结束,那么这个异常将被Future.get封装在ExecutorException中重新抛出。
关闭钩子
正常关闭的触发方式:
- 最后一个“正常(非守护)”线程结束
- 调用System.exit
- Ctrl + C
public class Hook {
public static void main(String[] args) {
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
@Override
public void run() {
System.out.println("!!");
}
}));
}
}
在start方法中注册关闭钩子
public void start() {
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
@Override
public void run() {
//TODO
}
}));
}