Java并发编程实战(学习笔记六 第七章 取消与关闭 下)

7.2 停止基于线程的服务 (Stopping a Thread-based Service)

应用程序通常会创建拥有多个线程的服务,例如线程池,并且这些服务的生命周期通常比创建它们的方法的生命周期更长。如果应用程序准备退出,那么这些服务所拥有的线程也需要结束。由于无法通过抢占式的方法来停止线程,因此它们需要自行结束。

正确的封装原则是:除非拥有某个线程,否则不能对该线程进行操控,例如中断线程或者修改线程的优先级等。线程有一个拥有者,即创建该线程的类,因此线程池是其工作者线程的所有者,如果要中断这些线程,那么应该使用线程池。

线程的所有权是不可传递的:应用程序可以拥有服务,服务也可以拥有工作者线程,但应用程序并不能拥有工作者线程,因此应用程序不能直接停止工作者线程。相反,服务应该提供生命周期方法(Lifecycle Method)来关闭它自己以及它所拥有的线程。这样,当应用程序关闭服务时,服务就可以关闭所有的线程了。ExecutorServi提供了shutdown和shutdownNow等方法。

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

7.2.1 示例:日志服务(A Logging Service)

在大多数服务器应用程序中都会用到日志,例如,在代码中插入println语句就是一种简单的日志。

在11.6节中,我们将看到这种内联日志功能会给一些高容量的(highvolume)应用程序带来一定的性能开销。另一种替代方法是通过调用log(日志)方法将日志消息放入某个队列中,并由其他线程来处理。

LogWriter给出了一个简单的日志服务示例,其中日志操作单独的日志线程中执行。产生日志消息的线程并不会将消息直接写入输出流,而是由LogWriter通过BlockingQueue将消息提交给日志线程,由日志线程写入。这是一种多生产者但消费者(Multiple-Producer,Single-Consumer)的设计方式:每个调用log的操作都相当于一个生成者,而后台的日志线程则相当于消费者。如果消费者的处理速度低于生产者的生成速度,那么BlockingQueue将阻塞生产者,直到日志线程有能力处理新的日志服务。

//         7-13 不支持关闭的生产者-消费者日志服务(不好)
public class LogWriter {
   
      private final BlockingQueue<String> queue;
      private final LoggerThread logger; //logger(记录器)
      private static final int CAPACITY = 1000;

      public LogWriter(Writer writer){
          this.queue=new LinkedBlockingQueue<>(CAPACITY);
          this.logger=new LoggerThread(writer);
      }

      public void start(){ //启动线程
          logger.start();
      }
      //充当生产者
      public void log(String msg)throws InterruptedException{
          queue.put(msg);
      }
      //产生日志消息的线程并不会将消息直接写入输出流,而是由LogWriter通过BlockingQueue将消息提交给日志线程,由日志线程写入。
      private class LoggerThread extends Thread{
      //日志线程
          private final PrintWriter writer;  //PrintWriter是线程安全的

          public LoggerThread(Writer writer) {
              this.writer = new PrintWriter(writer, true); // true参数代表了autoflush
          }

          public void run(){
              try{
                  while(true)
                      writer.println(queue.take());   
              }catch (InterruptedException ignored) {
              }finally{
                writer.close();
              }
          }
      }
}

为了使LoggerWriter这样的服务在软件产品中能发挥实际的作用,还需要实现一种终止日志线程的方法,从而避免使JVM无法正常关闭。

要停止日志线程是很容易的,因为它会反复调用take,而take能响应中断。如果将日志线程修改为当捕获到InterruptedException时退出,那么只需中断日志线程就能停止服务。然而,如果过只是时日志线程退出,还不是一种完备的关闭机制。这种直接关闭的做法会丢失那些正在等待被写入到日志的信息,不仅如此,其他线程将在调用log时被阻塞,因为日志消息队列是满的(日志线程停止了take),因此这些线程将无法解除阻塞状态。当取消一个生产者-消费者操作时,需要同时取消生产者和消费者,在中断日志线程过程时会处理消费者,但在这个示例中,由于生产者不是专门的线程,因此要取消它们将非常困难。

另一种关闭LogWriter的方法是:设置某个“已请求关闭”标志(与前面的已请求取消标志类似),避免进一步提交日志信息。

如7-14所示,在收到关闭请求后,消费者会把队列中的所有信息系写入日志,并解除所有在调用log时阻塞的生产者。然而,在这个方法中存在竞态条件问题,使得该方法并不可靠。log的实现是一种“先检查后执行”的代码序列:生产者发现服务还没有关闭,因此在关闭服务后仍然会把日志信息放入队列,这同样会使得生产者可能在调用log时阻塞并且无法解除阻塞状态(即检查的时候还未停止,而put的时候停止了,这个时候将会阻塞)。

//          7-14    通过一种不可靠的方式为日志增加关闭支持
public void log(String msg) throws InterruptedException {
  //存在“先检查后执行”的竞态条件
     if (!shutdownRequested)    //如果已请求关闭标志未设置,则正常生成
          queue.put(msg);
     else                       //否则抛出异常
           throw new IllegalStateException("logger is shut down");
     }

可以通过一些技巧来降低这些情况发生的概率(例如,在宣布队列被情况之前,让消费这等待数秒),但仍没有解决问题的本质。

为LogWriter提供可靠关闭操作的方法是解决竞态条件问题,因而要使日志消息的提交操作成为原子操作。然而,我们不希望在消息加入队列时去持有一个锁,因为put方法本省就可以阻塞。我们采用的方法是,通过原子方式来检查关闭请求,并且有条件地递增一个计数器来“保持”提交信息的权利

//           7-15  向LogWriter添加可靠的取消操作
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) {
         this.queue = new LinkedBlockingQueue<String>();
         this.loggerThread = new LoggerThread();
         this.writer = new PrintWriter(writer);
     }

     public void start(){
         loggerThread.start();
     }

     public void stop(){
         
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值