第七章:取消与关闭——Java并发编程实战

Java没有提供任何机制来安全地终止线程(虽然Thread.stop和suspend方法提供了这样的机制,但由于存在缺陷,因此应该避免使用

中断:一种协作机制,能够使一个线程终止另一个线程的当前工作

立即停止会使共享的数据结构处于不一致的状态,需要停止时,发出中断请求,被要求中断的线程处理完他当前的任务后会自己判断是否停下来

 

一、任务取消

若外部代码能在某个操作正常完成之前将其置入“完成”状态,则还操作是可取消的。(用户请求取消、有时间限制的操作<并发查找结果,一个线程找到后可取消其他线程>、应用程序事件、错误、关闭)

取消策略:详细地定义取消操作的“How”、“When”以及“What”,即其他代码如何(How)请求取消该任务,任务在何时(When)检查是否已经请求了取消,以及在响应取消请求时应该执行哪些(What)操作

举例:设置volatile变量为取消标志,每次执行前检查

 1 private volatile boolean canceled;
 2     
 3     @Override
 4     public void run() {
 5         BigInteger p = BigInteger.ONE;
 6         while (!canceled){
 7             p = p.nextProbablePrime();
 8             synchronized (this) { //同步添加素数
 9                 primes.add(p);
10             }
11         }
12     }

注意:这是一个有问题的取消方式,若线程阻塞在add操作后,那么即使设置了取消状态,它也不会运行到检验阻塞状态的代码,因此会永远阻塞

1、中断

  线程可以通过这种机制来通知另一个线程,告诉它在合适的或者可能的情况下停止当前工作,并转而执行其他的工作。(在取消之外的其他操作使用中断都是不合适的)

  调用interrupt并不意味者立即停止目标线程正在进行的工作,而只是传递了请求中断的消息。会在下一个取消点中断自己,如wait, sleep,join等

1 public class Thread {
2      public void interrupt() { ... }//中断目标线程,恢复中断状态
3      public boolean isInterrupted() { ... }//返回目标线程的中断状态
4      public static boolean interrupted() { ... }//清除当前线程的中断状态,并返回它之前的值(用于已经设置了中断状态,但还尚未相应中断)
5      ...
6  }

阻塞库方法,例如Thread.sleep和Object.wait等,都会检查线程何时中断,并且在发现时提前返回。它们在响应中断时执行的操作包括 : 清除中断状态,抛出InterruptedException,表示阻塞操作由于中断而提前结束

 

  • 显示的检测中断!Thread.currentThread().isInterrupted()后推出
  • 阻塞方法中抓到InterruptedException后退出

2、中断策略——规定线程如何解释某个中断请求——当发现中断请求时,应该做哪些工作

  由于每个线程拥有各自的中断策略,因此除非你知道中断对该线程的含义,否则就不应该中断这个线程。

3、响应中断

  • 传递异常(throws InterruptedException)

  • 恢复中断状态,从而事调用栈的上层代码能够对其进行处理。(Thread.currentThread().interrupt();)

4、通过Future实现取消

  boolean cancel(boolean mayInterruptIfRunning);

  • 如果任务已完成、或已取消,或者由于某些其他原因而无法取消,则此尝试将失败,返回false
  • 调用cancel时,如果调用成功,而此任务尚未启动,则此任务将永不运行
  • 如果任务已经执行,mayInterruptIfRunning参数决定了是否向执行任务的线程发出interrupt操作

5、处理不可中断的阻塞——对于某些阻塞操作,只是设置了中断状态

  • Java.io包中的同步Socket I/O。虽然InputStream和OutputStream中的read和write等方法都不会响应中断,但通过关闭底层的套接字,可以使得由于执行read或write等方法而被阻塞的线程抛出一个SocketException。
  • Java.io包中的同步I/O。当中断一个正在InterruptibleChannel上等待的线程时,将抛出ClosedByInterruptedException)并关闭链路(这还会使得其他在这条链路上阻塞的线程同样抛出ClosedByInterruptException)。当关闭一个InterruptibleChannel时,将导致所有在链路操作上阻塞的线程抛出AsynchronousCloseException。大多数标准的Channel都实现了InterruptibleChannel。
  • Selector的异步I/O。如果一个线程在调用Selector.select方法(在java.nio.channels中)时阻塞了,那么调用close或wakeup方法会使线程抛出ClosedSelectorException并提前返回。
  • 获取某个锁。如果一个线程由于等待某个内置锁而被阻塞,那么将无法响应中断,因为线程认为它肯定获得锁,所以将不会理会中断请求。但是,在Lock类中提供了lockInterruptibly方法,该方法允许在等待一个锁的同时仍能响应中断。
 1 //改写interrupt方法发出中断请求   
 2  @Override
 3     public void interrupt() {
 4         try {
 5             socket.close(); //中断前关闭socket
 6         } catch (IOException e) {
 7             
 8         } finally{
 9             super.interrupt();
10         }
11     }

6、采用newTaskFor来封装非标准的取消

 

二、停止基于线程的服务

应用程序通常会创建基于线程的服务,如线程池。这些服务的时间一般比创建它的方法更长。

  • 服务退出 -> 线程需要结束  无法通过抢占式的方法来停止线程,因此它们需要自行结束
  • 除非拥有某个线程,否则不能对该线程进行操控。例如,中断线程或者修改线程的优先级等
  • 线程池是其工作者线程的所有者,如果要中断这些线程,那么应该使用线程池
  • 应用程序可以拥有服务,服务也可以拥有工作者线程,但应用程序不能拥有工作者线程,因此应用程序不能直接停止工作者线程。

服务应该生命周期方法关闭它自己以及他拥有的线程

  • 要服务的存在时间大于创建线程的方法的存在时间,那么就应该提供生命周期方法
  • ExecutorService提供的shutdown(), shutdownNow()

1、示例:日志服务

 1 // LogWriter就是一个基于线程的服务,但不是一个完成的服务
 2 public class LogWriter {
 3     //日志缓存
 4     private final BlockingQueue<String> queue;
 5     private final LoggerThread logger;//日志写线程
 6 private static final int CAPACITY = 1000;
 7 
 8     public LogWriter(Writer writer) {
 9         this.queue = new LinkedBlockingQueue<String>(CAPACITY);
10         this.logger = new LoggerThread(writer);
11     }
12 
13 public void start() { logger.start(); }
14 
15     //应用程序向日志缓存中放入要记录的日志
16     public void log(String msg) throws InterruptedException {
17         queue.put(msg);
18 }
19 
20     //日志写入线程,这是一个多生产者,单消费者的设计
21     private class LoggerThread extends Thread {
22         private final PrintWriter writer;
23         public LoggerThread(Writer writer) {
24             this.writer = new PrintWriter(writer, true); // autoflush
25         }
26         public void run() {
27             try {
28                 while (true)
29                    writer.println(queue.take());
30             } catch(InterruptedException ignored) {
31             } finally {
32                 writer.close();
33             }
34         }
35     }
36 }

注意:可以中断阻塞的take()方法停止日志线程(消费者线程),但生产者没有专门的线程,没办法取消

 1 //日志服务,提供记录日志的服务,并有管理服务生命周期的相关方法
 2 public class LogService {
 3        private final BlockingQueue<String> queue;
 4        private final LoggerThread loggerThread;// 日志写线程
 5        private final PrintWriter writer;
 6        private boolean isShutdown;// 服务关闭标示
 7        // 队列中的日志消息存储数量。我们不是可以通过queue.size()来获取吗?
 8        // 为什么还需要这个?请看后面
 9        private int reservations;
10 
11        public LogService(Writer writer) {
12               this.queue = new LinkedBlockingQueue<String>();
13               this.loggerThread = new LoggerThread();
14               this.writer = new PrintWriter(writer);
15 
16        }
17 
18        //启动日志服务
19        public void start() {
20               loggerThread.start();
21        }
22 
23        //关闭日志服务
24        public void stop() {
25               synchronized (this) {
26                      /*
27                       * 为了线程可见性,这里一定要加上同步,当然volatile也可,
28                       * 但下面方法还需要原子性,所以这里就直接使用了synchronized,
29                       * 但不是将isShutdown定义为volatile
30                       */
31                      isShutdown = true;
32               }
33               //向日志线程发出中断请求
34               loggerThread.interrupt();
35        }
36 
37        //供应用程序调用,用来向日志缓存存放要记录的日志信息
38        public void log(String msg) throws InterruptedException {
39               synchronized (this) {
40                      /*
41                       * 如果应用程序发出了服务关闭请求,则不存在接受日志,而是直接
42                       * 抛出异常,让应用程序知道
43                       */
44                      if (isShutdown)
45                             throw new IllegalStateException(/*日志服务已关闭*/);
46                      /*
47                       * 由于queue是线程安全的阻塞队列,所以不需要同步(同步也可
48                       * 但并发效率会下降,所以将它放到了同步块外)。但是这里是的
49                       * 操作序列是由两个操作组成的:即先判断isShutdown,再向缓存
50                       * 中放入消息,如果将queue.put(msg)放在同步外,则在多线程环
51                       * 境中,LoggerThread中的  queue.size() == 0 将会不准确,所
52                       * 以又要想queue.put不同步,又要想queue.size()计算准确,所
53                       * 以就使用了一个变量reservations专用来记录缓存中日志条数,
54                       * 这样就即解决了同步queue效率低的问题,又解决了安全性问题,
55                       * 这真是两全其美
56                       */
57                      //queue.put(msg);
58                      ++reservations;//存储量加1
59               }
60               queue.put(msg);
61        }
62 
63        private class LoggerThread extends Thread {
64               public void run() {
65                      try {
66                             while (true) {
67                                    try {
68                                           synchronized (LogService.this) {
69                                                  // 由于 queue 未同步,所以这里不能使用queue.size
70                                                  //if (isShutdown && queue.size() == 0)
71 
72                                                  // 如果已关闭,且缓存中的日志信息都已写入,则退出日志线程
73                                                  if (isShutdown && reservations == 0)
74                                                         break;
75                                           }
76                                           String msg = queue.take();
77                                           synchronized (LogService.this) {
78                                                  --reservations;
79                                           }
80                                           writer.println(msg);
81                                    } catch (InterruptedException e) { /* 重试 */
82                                    }
83                             }
84                      } finally {
85                             writer.close();
86                      }
87               }
88        }
89 }

注意:通过原子方式来检查关闭请求,并且有条件地递增一个计数器来“保持”提提交消息的权利

2、关闭ExecutorService

  shutdown():启动一次顺序关闭,执行完以前提交的任务,没有执行完的任务继续执行完

  shutdownNow():试图停止所有正在执行的任务(向它们发出interrupt操作语法,无法保证能够停止正在处理的任务线程,但是会尽力尝试),并暂停处理正在等待的任务,并返回等待执行的任务列表。

  ExecutorService已关闭,再向它提交任务时会抛RejectedExecutionException异常

3、“毒丸”对象——当得到这个对象时,立即停止

  在提交“毒丸”对象之前提交的所有工作都会被处理,而生产者在提交了“毒丸”对象后,将不会再提交任何工作

4、只执行一次的服务

  如果某个方法需要处理一批任务,并且当所有任务都处理完成后才返回,那么可以通过一次私有的Executor来简化服务的生命周期管理,其中该Executor的生命周期是由这个方法来控制的。

 1 boolean checkMail(Set<String> hosts, long timeout, TimeUnit unit)
 2         throws InterruptedException {
 3 ExecutorService exec = Executors.newCachedThreadPool();
 4 //这里不能使用 volatile hasNewMail,因为还需要在匿名内中修改
 5     final AtomicBoolean hasNewMail = new AtomicBoolean(false);
 6     try {
 7         for (final String host : hosts)//循环检索每台主机
 8             exec.execute(new Runnable() {//执行任务
 9                 public void run() {
10                    if (checkMail(host))
11                        hasNewMail.set(true);
12                 }
13             });
14     } finally {
15         exec.shutdown();//因为ExecutorService只在这个方法中服务,所以完成后即可关闭
16         exec.awaitTermination(timeout, unit);//等待任务的完成,如果超时还未完成也会返回
17     }
18     return hasNewMail.get();
19 }

5、shutdown的局限性

我们无法通过常规方法来找出哪些任务已经开始但尚未结束。这意味着我们无法在关闭过程中知道正在执行的任务的状态,除非任务本身会执行某种检查

 

 1 public class TrackingExecutor extends AbstractExecutorService {
 2     private final ExecutorService exec;
 3     private final Set<Runnable> tasksCancelledAtShutdown =
 4             Collections.synchronizedSet(new HashSet<Runnable>());
 5 
 6     public TrackingExecutor(ExecutorService exec) {
 7         this.exec = exec;
 8     }
 9 
10     public List<Runnable> getCancelledTasks() {//返回被取消的任务
11         if (!exec.isTerminated())//如果shutdownNow未调用或调用未完成时
12             throw new IllegalStateException(/*...*/);
13         return new ArrayList<Runnable>(tasksCancelledAtShutdown);
14     }
15 
16     public void execute(final Runnable runnable) {
17         exec.execute(new Runnable() {
18             public void run() {
19                 try {
20                     runnable.run();
21                             /*参考:http://blog.csdn.net/coslay/article/details/48038795
22                              * 实质上在这里会有线程安全性问题,存在着竞争条件,比如程序刚
23                              * 好运行到这里,即任务任务(run方法)刚好运行完,这时外界调用
24                              * 了shutdownNow(),这时下面finally块中的判断会有出错,明显示
25                              * 任务已执行完成,但判断给出的是被取消了。如果要想安全,就不
26                              * 应该让shutdownNow在run方法运行完成与下面判断前调用。我们要
27                              * 将runnable.run()与下面的if放在一个同步块、而且还要将
28                              *  shutdownNow的调用也放同步块里并且与前面要是同一个监视器锁,
29                              *  这样好像就可以解决了,不知道对不能。书上也没有说能不能解决,
30                              *  只是说有这个问题!但反过来想,如果真的这样同步了,那又会带
31                              *  性能上的问题,因为什么所有的任务都会串形执行,这样还要
32                              *  ExecutorService线程池干嘛呢?我想这就是后面作者为什么所说
33                              *  这是“不可避免的竞争条件”
34                              */
35                 } finally {
36                                    //如果调用了shutdownNow且运行的任务被中断
37                     if (isShutdown()
38                             && Thread.currentThread().isInterrupted())
39                         tasksCancelledAtShutdown.add(runnable);//记录被取消的任务
40                 }
41             }
42         });
43 }
44 // 将ExecutorService 中的其他方法委托到exec
45 }

 

 

三、处理非正常的线程终止

  在一个线程中启动另一个线程,另一个线程中抛出异常,如果没有捕获它,这个异常也不会传递到父线程中

  任何代码都可能抛出一个RuntimeException。每当调用另一个方法时,都要对它的行为保持怀疑,不要盲目地认为它一定会正常返回,或者一定会抛出在方法原型中声明的某个已检查异常

 1 //如果任务抛出了一个运行时异常,它将允许线程终结,但是会首先通知框架:线程已经终结
 2 public void run() {//工作者线程的实现
 3     Throwable thrown = null;
 4     try {
 5         while (!isInterrupted())
 6             runTask(getTaskFromWorkQueue());
 7     } catch (Throwable e) {//为了安全,捕获的所有异常
 8         thrown = e;//保留异常信息
 9     } finally {
10         threadExited(this, thrown);// 重新将异常抛给框架后终结工作线程
11     }
12 }

未捕获异常的线程

在Thread API中提供了UncaughtExceptionHandler,它能检测出某个线程由于未捕获的异常而终结的情况

在运行时间较长的应用程序中,通常会为所有的未捕获异常指定同一个异常处理器,并且该处理器至少会将异常信息记录到日志中。

public class UEHLogger implements Thread.UncaughtExceptionHandler {
    public void uncaughtException(Thread t, Throwable e) {
        Logger logger = Logger.getAnonymousLogger();
        logger.log(Level.SEVERE, "Thread terminated with exception: " + t.getName(), e);
    }
}

 

四、JVM关闭

JVM既可通过正常手段来关闭,也可强行关闭。

  • 正常关闭:当最后一个“正常(非守护)”线程结束时、当有人调用了System.exit时、或者通过其他特定于平台的方法关闭时
  • 强行关闭:Runtime.halt,这种强行关闭方式将无法保证是否将运行关闭钩子

1、关闭钩子

  • 关闭钩子是指通过Runnable.addShutdownHook注册的但尚未开始的线程
  • JVM并不能保证关闭钩子的调用顺序
  • 当所有的关闭钩子都执行结束时,如果runFinalizersOnExit为true,那么JVM将运行终结器(finalize),然后再停止
  • JVM并不会停止或中断任何在关闭时仍然运行的应用程序线程。当JVM最终结束时,这些线程将被强行结束。如果关闭钩子或终结器没有执行完成,那么正常关闭进程“挂起”并且JVM必须被强行关闭。当被强行关闭时,只是关闭JVM,而不会运行关闭钩子
  • 关闭钩子应该是线程安全的
  • 关闭钩子必须尽快退出,因为它们会延迟JVM的结束时间
public void start()//通过注册关闭钩子,停止日志服务
{
    Runnable.getRuntime().addShutdownHook(new Thread(){
        public void run()
        {
            try{LogService.this.stop();}
            catch(InterruptedException ignored){}
        }
    });
}

2、守护线程——一个线程来执行一些辅助工作,但有不希望这个线程阻碍JVM的关闭

  线程可分为两种:普通线程和守护线程。在JVM启动时创建的所有线程中,除了主线程以外,其他的线程都是守护线程

  普通线程与守护线程之间的差异仅在于当线程退出时发生的操作。当一个线程退出时,JVM会检查其他正在运行的线程,如果这些线程都是守护线程,那么JVM会正常退出操作。当JVM停止时,所有仍然存在的守护线程都将被抛弃——既不会执行finally代码块,也不会执行回卷栈,而JVM只是直接退出

3、终结器(清理文件句柄或套接字句柄等)——避免使用

  垃圾回收器对那些定义了finalize方法的对象会进行特殊处理:在回收器释放它们后,调用它们的finalize方法,从而确保一些持久化的资源被释放。

  通过使用finally代码块和显式的close方法,能够比使用终结器更好地管理资源

例外:当需要管理对象时,并且该对象持有的资源是通过本地方法获得的

 

posted @ 2016-11-05 22:30  HectorHou  阅读(1196)  评论(1编辑  收藏  举报