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

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

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

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值