Loading

Java并发学习之停止基于线程的服务

一.停止基于线程的服务

1.一般的应用程序中会创建多个线程的服务,例如线程池,我们创建一个线程池的服务时,它会又创建多个线程,如果我们不关闭服务,它会一直存在,所以服务的生命周期通常比创建它们的方法的生命周期更长。如果应用程序准备退出,应用程序会关闭服务,服务又会关闭它拥有的线程。由于无法通过抢占式的方法来停止线程,所以它们需要自行关闭。

2.我们对线程的封装原则是:除非拥有某个线程,否则不能对该线程进行操控。线程有一个相应的所有者(创建它的类),因此线程池是其他工作者线程的所有者,如果要中断这些线程,那么应该使用线程池来中断。

3.与其他的封装对象一样,线程的所有权是不可传递的:应用程序可以拥有服务,服务也可以拥有工作者线程,但应用程序不能拥有工作者线程,因此应用程序不能直接停止工作者线程。(不可以越级操作)。

4.所以服务应该提供生命周期方法来关闭它自己以及它拥有的所有线程。这样当应用程序关闭该服务时,服务就可以关闭所有的线程了。

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

二.示例学习:日志服务

在大多数应用程序中会用到日志,比如我们开始学习编程时,经常使用println语句来打印我们希望看到的信息来调试程序。 像PrintWriter这样的字符流类是线程安全的, 因此这种简单的方法不需要显式的同步。但是这种方式需要 一定的性能开销。
另一种代替方法是通过调用log方法将日志消息放入某个队列中,并由其他线程来处理。

1.不支持关闭的生产者 --消费者日志服务

public class LogWriter{
   
  private final BlockingQueue<String> queue;//声明一个阻塞队列
  private final LoggerThread logger;//声明一个线程对象

  //初始化声明的对象
  public LogWriter(Writer writer){
   
    this.queue=new LinkedBlockingQueue<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 void run(){
   
      try{
   
        while(true)
           writer.println(queue.take());//打印出队列中的信息
      }catch(InterruptedException ignored){
   
      
      }finally{
   
        writer.close();
      }
    }
  }
}

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

找出问题: 我们可以看到上面的日志线程对于中断的处理是选择忽略不处理,那么如果我们人为中断的话,那么剩余的信息该如何处理呢?所以说,对于中断处理忽略的做法是不正确的。

2)为了使像LogWriter这样的服务在软件产品中能发挥实际的作用,还需要实现一种终止日志线程的方法,从而避免使JVM无法正常关闭,要停止日志线程是很容易的,因为它会反复调用take,而take能响应中断,如果将日志线程修改为当捕获到InterruptedException时退出,那么只需要中断日志线程就能停止服务。
但是如果只使日志线程退出,这还不是一种完备的关闭机制,这种直接关闭的做法会丢失那些正在等待写入到日志的信息,不仅如此,其他线程将在调用log时可能被阻塞,因为日志消息队列可能是满的,因此这些线程将无法解除阻塞状态。

当取消一个生产者–消费者操作时,需要同时取消生产者和消费者。在中断日志线程时会处理消费者,但在上面的实例中,由于生产者并不是专门的线程,因此要取消它们将非常困难。

3)另一种关闭LogWriter的方法是:设置某个 “已请求关闭” 标志,以避免进一步提交日志消息。

public void log(String msg) throws InterruptedException{
   
   if(!shutdownRequested)
         queue.put(msg);
   else
       throw new IllegalStateException("logger is shut down");
}

我们设置一个shutdownRequested的标志,在收到关闭请求后,消费者会把队列中的所有消息写入日志,并解除所有在调用log时阻塞的生产者。

然而这个方法存在着竞态条件问题使得该方法并不可靠哦:上面的log的实现是一种 “先判断再运行” 的代码序列,生产者发现该服务还没有关闭,因此在关闭服务后仍然可能会将日志消息放入队列,这同样会使得生产者可能在调用log时阻塞并且无法解除阻塞状态。

4)所以为LogWriter提供可靠关闭操作的方法是解决竟态条件问题,因而要使日志消息的提交操作成为原子操作,但是我们不希望再消息加入队列时去持有一个锁,因为put方法本身就可以阻塞,所以我们可以采用:通过原子方式来检查关闭请求,并且有条件的递增一个计数器来保持提交信息的权力,如下:

public class LogService{
   
   private final BlockingQueue<String> queue;//消息队列
   private final LoggerThread loggerThread;//日志线程
   private final PrintWriter writer;//输出流
   private boolean isShutdown;//终止标志
   private int reservations;//计数器
   
   //开始日志线程
   publi
posted @ 2020-11-24 15:28  文牧之  阅读(20)  评论(0)    收藏  举报  来源