停止基于线程的服务(一)

停止基于线程的服务

应用程序通常会创建拥有多个线程的服务,如 线程池 即可以表示一种拥有多个线程的服务。应用程序退出时,这些服务所拥有的线程也应该结束,JVM 才能被正常关闭。

在 Java 中,线程由 Thread 对象表示,且和其它对象一样可以被自由共享。此外,线程有其所有者,即创建该线程的类,如线程池即是其工作者线程的所有者,应该通过线程的所有者来操控它们。

和其它封装对象一样,线程的所有权是不可传递的:应用程序可以拥有服务,服务也可以拥有工作者线程,但应用程序不能拥有工作者线程,因此应用程序不能直接停止工作者线程。相反,服务应该提供生命周期方法(Lifecycle Method)来关闭它自己以及它所拥有的线程。如在 ExecutorService 中提供了 shutdown 和 shutdownNow 等生命周期方法。

总之,对于持有线程的服务,只要服务的存在时间大于创建线程的方法的存在时间,那么就应该提供生命周期方法。

日志服务

LogWriter

LogWriter 提供了一种简单的日志服务示例。该示例中,产生日志的线程不会直接将消息写入输出流,而是由 LogWriter 通过 BlockingQueue 将消息提交到日志线程,并由日志线程写入到输出流。这是一种多生产者单消费者的设计模式:每个调用 log 的操作都相当于生产者,而后台的日志线程相当于消费者。如果消费者的处理速度低于生产者的生产速度,那么 BlockingQueue 将阻塞生产者。LogWriter 相当于一种日志服务,而真正负责写入日志消息的则是该服务所拥有的工作线程。LogWriter 并没有提供关闭服务的方法可以使得结束服务的同时关闭其所持有的工作线程。

public class LogWriter {
    private final BlockingQueue<String> queue;
    private final LoggerThread logger;
    private final int CAPACITY = 100;
    public LogWriter(PrintWriter writer) {
        this.queue = new LinkedBlockingDeque<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 LoggerThread(PrintWriter writer) {
            this.writer = writer;
        }
        public void run() {
            try {
                while (true) {
                    writer.println(queue.take());
                }
            } catch (InterruptedException ignored) {

            } finally {
                writer.close();
            }
         }
    }
}

ClosableLogWriter

为 LogWriter 添加关闭服务的生命周期方法有多种方式,下面详细讨论这些方式的利弊:

第一种方式是利用线程中断:由于日志线程会反复调用 take,而 take 可以响应中断。如果日志线程在捕捉到 InterruptedException 时的处理机制是退出,那么只需中断日志线程即可。然而这种方式并不完备,因为直接关闭的做法会丢失正在等待被写入到日志的信息,此外,其它线程在调用 log 时会被阻塞,且阻塞状态无法解除。

还有一种方式是设置某个“请求关闭”标志:通过判断该标志可以避免其它线程继续提交日志消息,当收到关闭请求后,消费者将队列中的所有消息写入日志,并解除所有在调用 log 时阻塞的生产者。但这种实现是一种 “先判断再运行” 的代码序列存在竞争条件的问题,仍然可能导致线程阻塞在 log 方法且无法解除的现象(可以通过一些技巧降低这类情况发生的概率,如在宣布队列被清空之前,让消费者等待数秒钟,但依然无法保证可靠性)。

为了解决竞态条件问题,要让日志的提交操作成为原子操作,但我们不希望在消息加入队列时去持有一个锁,因为 put 方法本身就可以阻塞。故采用的方法是:通过原子方式来检查关闭请求,并且有条件地递增一个计数器来“保持”提交消息的权力。ClosableLogWriter 实现了这一策略。

public class ClosableLogWriter {
    private final BlockingQueue<String> queue;
    private final PrintWriter writer;
    private final LoggerThread loggerThread;
    private final int CAPACITY = 100;
    @GuardedBy("this") private boolean isShutdown;
    @GuardedBy("this") private int reservations;
    
    public ClosableLogWriter(PrintWriter writer) {
        this.queue = new LinkedBlockingDeque<>(CAPACITY);
        this.writer = writer;
        this.loggerThread = new LoggerThread();
    }
    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 (ClosableLogWriter.this) {
                            if (isShutdown && reservations == 0)
                                break;
                        }
                        String msg = queue.take();
                        synchronized (ClosableLogWriter.this) {
                            --reservations;
                        }
                        writer.println(msg);
                    } catch (InterruptedException e) {/* retry */}

                }
            } finally {
                writer.close();
            }
        }
    }
}

LogService

LogService 将管理工作线程的工作委托给了 ExecutorService,而不是由其自行管理。

public class LogService {
    private final PrintWriter writer;
    private final ExecutorService exec = Executors.newSingleThreadExecutor();
    private final int TIMEOUT = 10;
    private final TimeUnit UNIT = TimeUnit.SECONDS;
    public void start() {}
    public LogService(PrintWriter writer) {
        this.writer = writer;
    }
    
    public void stop() throws InterruptedException {
        try {
            exec.shutdown();
            exec.awaitTermination(TIMEOUT, UNIT);
        } finally {

        }
    }
    
    public void log(String msg) {
        try {
            exec.execute(new WriteTask(msg));
        } catch(RejectedExecutionException ignored) {}
    }

    private class WriteTask implements Runnable {
        private final String msg;
        public WriteTask(String msg) {
            this.msg = msg;
        }
        public void run() {
            writer.println(msg);
        }
    }
}
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值