如果应用程序准备退出,那么这些服务所拥有的线程也需要结束,本节将讲述以下技巧:
- 关闭日志服务
– 不支持关闭的日志服务
– 通过一种不可靠的方式增加关闭操作
– 可靠的取消操作 - 关闭ExecutorService
- 毒丸对象
- 只执行一次的服务
- shutdownNow的局限性
– 在ExecutorService跟踪在关闭之后取消的任务
– 使用TrackingExecutorService来保存未完成的任务
关闭日志服务
不可关闭的日志服务
基于生产者消费者的日志服务
public class LogWriter {
private final BlockingQueue<String> queue;
private final LoggerThread logger;
public LogWriter(Writer writer){
this.queue = new LinkedBlockingQueue<>();
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 LoggerThread(Writer writer) {
this.writer = (PrintWriter) writer;
}
//....
@Override
public void run() {
try{
while(true){
writer.println(queue.take());
}
}catch (InterruptedException e){
//...
}finally{
writer.close();
}
}
}
}
为了使像LogWriter这样的服务在软件产品中能发挥实际作用,还需要实现一种终止日志线程的方法,从而避免使JVM无法正常关闭.
不可靠的方式增加关闭服务
如果将日志线程修改为捕获到InterruptedException时退出,那么只需要中断日志线程就能停止服务.
public void log(String msg) throws InterruptedException{
if(!isShutdown())
queue.put(msg);
else
throw new IllegalStateException("logger is shut down");
}
public void shutdown(){
logger.interrupt();
}
public boolean isShutdown(){
return !logger.isAlive();
}
这种直接关闭的做法会丢失那些正在等待被写入到日志的信息,不仅如此,其他线程将在调用log时被阻塞,因为日志消息队列是满的,因此这些线程无法解除阻塞状态.
向LogWriter添加可靠的取消操作
为LogWriter提供一个可靠关闭操作的方法是解决竞态条件问题,因而要是日志消息的提操作成为原子操作.然而,我们不希望在消息加入队列时去持有一个锁,因为put方法本身就可以阻塞.
通过原子方式来检查关闭请求,并且有条件地递增一个计数器来保持提交消息的权利.
public class LogService {
private final BlockingQueue<String> queue;
private final LoggerThread loggerThread;
private final PrintWriter writer;
private boolean isShutdown;
private int reservations;
public LogService(Writer writer){
queue = new LinkedBlockingQueue<>();
loggerThread = new LoggerThread();
this.writer = (PrintWriter) writer;
isShutdown = false;
reservations = 0;
}
public void start(){
loggerThread.start();
}
public void log(String msg) throws InterruptedException{
synchronized (this) {
if(isShutdown){
throw new IllegalStateException("logger is shut down");
}
++reservations;
}
queue.put(msg);
}
public void stop(){
synchronized (this) {
isShutdown = true;
}
loggerThread.interrupt();
}
private class LoggerThread extends Thread{
//....
@Override
public void run() {
try{
while(true){
synchronized (LogService.this) {
if(isShutdown && reservations==0){
break;
}
}
String msg = queue.take();
synchronized (LogService.this) {
--reservations;
}
writer.println(msg);
}
}catch (InterruptedException e){
//...
}finally{
writer.close();
}
}
}
}
关闭ExecutorService
简单的程序可以直接在main函数中启动和关闭全局的Executor,而在复杂的程序中,通常会将ExecutorService封装在某个更高级别的服务中,并且该服务能提供其自己的生命周期的办法.
try{
exec.shutdown();
exec.awaitTermination(TIMEOUT,UNIT);
}finally{
writer.close();
}
毒丸对象
另一种生产者消费者服务的方式就是使用毒丸对象: 毒丸是指放在队列上的一个对象,其含义是: 当得到这个对象时,立即停止.
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 File root;
public IndexingService(File root) {
queue = new LinkedBlockingQueue<>();
this.root = root;
}
public void start(){
producer.start();
consumer.start();
}
public void stop(){
producer.interrupt();
}
public void awaitTermination() throws InterruptedException{
consumer.join();
}
public class CrawlerThread extends Thread{
@Override
public void run() {
try{
crawl(root);
}catch(InterruptedException e){
/*发生异常*/
}finally{
while(true){
try{
queue.put(POISON);
break;
}catch(InterruptedException e){
/* 重新尝试 */
}
}
}
}
private void crawl(File root) throws InterruptedException{
// ...
}
}
public class IndexerThread extends Thread{
@Override
public void run() {
try{
while(true){
File file = queue.take();
if(file==POISON){
break;
}
else
IndexFile(file);
}
}catch(InterruptedException e){
/*发生异常*/
}
}
private void IndexFile(File file) {
//...
}
}
}
只有在生产者消费者的数量都已知的情况下,才可以使用”毒丸”对象.
只执行一次的服务
如果某个方法需要处理一匹任务,并且当所有的任务都处理完后才返回,那么可以通过一个私有的Executor来简化服务的生命周期.
boolean checkEmail(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(checkMail(host)){
hasNewMail.set(true);
}
}
});
}
}finally{
exec.shutdown();
exec.awaitTermination(timeout, unit);
}
return hasNewMail.get();
}
protected boolean checkMail(String host) {
return false;
}
shutdownNow的局限性
通过shutdownNow来强行关闭ExecutorService时,它会尝试取消正在执行的任务,并返回所有已提交但尚未开始的任务,从而我们可以将这些任务写入日志或保存起来以便之后进行处理.但是,它并不会返回正在执行,但是没有执行完毕的任务.
在ExecutorService中跟踪在关闭之后取消的任务
TrackingExecutor可以找出哪些任务已经开始,还没有完成.在所有设计良好的任务中,都会实现这个功能.
public class TrackingExecutor {
private final ExecutorService exec;
private final Set<Runnable> taskCancelledAtShutdown = 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<>(taskCancelledAtShutdown);
}
public void execute(final Runnable runnable){
exec.execute(new Runnable(){
public void run() {
try{
runnable.run();
}finally {
if(isShutdown()&&Thread.currentThread().isInterrupted()){
taskCancelledAtShutdown.add(runnable);
}
}
}
});
}
//将ExecutorService的其他方法委托给exec
protected boolean isShutdown() {
return exec.isShutdown();
}
}
使用TrackingExecutorService来保存未完成的任务
网页爬虫程序的工作通常是无穷无尽的,因此当爬虫程序必须关闭时,我们通常希望保存它的状态,以便稍后重新启动.
public abstract class WebCrawler {
private static final String TIMEOUT = null;
private static final String UNIT = null;
private volatile TrackingExecutor exec;
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(){
try{
saveUncrawled(exec.shutdownNow());
if(exec.awaitTermination(TIMEOUT,UNIT))
saveUncrawled(exec.getCancelledTask());
}finally {
exec = null;
}
}
public void submitCrawlTask(URL link) {
exec.execute(new CrawlTask(link));
}
private void saveUncrawled(List<Runnable> c) {
for (Runnable task : c) {
urlsToCrawl.add(((CrawlTask) task).getPage());
}
}
protected abstract List<URL> processPage(URL url);
private class CrawlTask implements Runnable{
private final URL url;
public CrawlTask(URL url) {
this.url = url;
}
@Override
public void run() {
for(URL link : processPage(url)){
if(Thread.currentThread().isInterrupted())
return ;
submitCrawlTask(link);
}
}
public URL getPage(){
return url;
}
}
}
在TranckingExecutor中存在一个不可避免的竞态条件,从而产生误报问题,一些被认为已取消的任务实际上已经执行完成.这个原因在于,在执行任务最后一条指令以及线程池将任务记录为”结束”的两个时刻之间,线程池可能被关闭.