多线程进阶006 之 停止基于线程服务

来源:互联网 发布:加百列和路西法知乎 编辑:程序博客网 时间:2024/05/17 00:16

如果应用程序准备退出,那么这些服务所拥有的线程也需要结束,本节将讲述以下技巧:

  • 关闭日志服务
    – 不支持关闭的日志服务
    – 通过一种不可靠的方式增加关闭操作
    – 可靠的取消操作
  • 关闭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中存在一个不可避免的竞态条件,从而产生误报问题,一些被认为已取消的任务实际上已经执行完成.这个原因在于,在执行任务最后一条指令以及线程池将任务记录为”结束”的两个时刻之间,线程池可能被关闭.