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(){