1、摘要

  让线程和任务启动很容易,但是,要安全地关闭它们,就不是那么容易了。

  Java中没有提供任何机制来安全地终止线程。

  中断(Interruption)只是一种协作机制,然一个线程告诉另一个线程:我想让你停下来。

  但是,另一个线程是否会停下来取决于它执行的任务里面是否响应了中断,并且采取了什么中断策略。

  一个良好的软件应该能很完善地处理失败、关闭和取消等生命周期过程。

 

2、任务取消

  取消某个操作的原因有很多:

    1、用户请求取消,如通过取消按钮

    2、有时间限制的操作,超时取消

    3、应用程序事件,一个事件发生后要求另一个事件取消。

    4、错误,出错取消

    5、关闭,平缓关闭(通过关闭机制退出)和立即关闭(崩溃、拔电源)

  Java中没有一种安全的抢占式方法来停止线程,只有协作式机制。

  一个可取消的任务必须拥有取消策略,该策略中详细规定:

    1、如何请求取消该任务

    2、任务在何时检查是否有取消操作

    3、响应该取消操作时应该执行那些操作

 

3、取消 - 通过保存取消标记并轮询

  通过轮询取消状态来取消有一个致命的问题:

    如果循环中执行的动作是阻塞的或需要长时间执行的,取消操作会被延迟,甚至不会发生!

/**
* 使用volatile类型来保存取消状态,
* 并轮询该状态来决定是否要退出
*/
@ThreadSafe 
public class PrimeGenerator implements Runnable { 
     @GuardedBy("this") 
     private final List<BigInteger> primes = new ArrayList<BigInteger>(); 
     private volatile boolean cancelled; 

     public void run() { 
         BigInteger p = BigInteger.ONE; 
         //通过轮询取消标志来进行取消
         while (!cancelled ) {     
                 p = p.nextProbablePrime(); 
                 synchronized (this) { 
                     primes.add(p); 
                 } 
         } 
     } 

     public void cancel() { cancelled = true; } 

     public synchronized List<BigInteger> get() { 
         return new ArrayList<BigInteger>(primes); 
     } 
  
//使用示例:运行一秒后取消
List aSecondOfPrimes() throws InterruptedException {
  PrimeGenerator generator = new PrimeGenerator();
  new Thread(generator).start();
  try {
    SECONDS.sleep(1);
  } finally {
    generator.cancel();
  }
  return generator.get();

 

 

/**
* 残缺的例子
* 生产者将货物放在一个阻塞队列中,消费者从该阻塞队列中取出货物
* 消费者在不需要货物时,将关闭生产者
* 但是,如果生产者的生产速率远大于消费速率,那么阻塞队列会被装满,并阻塞
* 此时生产者就无法检测到取消操作了
*/
class BrokenPrimeProducer extends Thread { 
     private final BlockingQueue<BigInteger> queue; 
     private volatile boolean cancelled = false; 

     BrokenPrimeProducer(BlockingQueue<BigInteger> queue) { 
         this.queue = queue; 
     } 

     public void run() { 
         try { 
             BigInteger p = BigInteger.ONE; 
             while (!cancelled) 
                 queue.put(p = p.nextProbablePrime());   //BlockingQueue.put() 是可阻塞的操作
         } catch (InterruptedException consumed) { } 
     } 

     public void cancel() { cancelled = true; } 
} 

void consumePrimes() throws InterruptedException { 
     BlockingQueue<BigInteger> primes = ...; 
     BrokenPrimeProducer producer = new BrokenPrimeProducer(primes); 
     producer.start(); 
     try { 
         while (needMorePrimes()) 
         consume(primes.take()); 
     } finally { 
         producer.cancel(); 
     } 
} 

 

 

4、取消 - 通过中断来取消

在Java语言规范中,没有将中断与任何取消语义关联起来,但是实际上,如果在取消操作之外的操作中使用中断,都不合适,并且很难支撑起大的应用。

 

  每一个线程都有一个boolean类型的中断状态,当中断线程时,该状态将被设为true。

  Thread类中包含了中断线程及查询线程中断状态的方法:

    interrupt() 方法能够中断目标线程;

    isInterrupted() 方法能够返回目标线程的中断状态。

    interrupted() 方法能够返回当前线程的中断状态,并情况该状态,这是清除中断状态的唯一方法。

  调用interrupted方法时要小心,因为它会清除中断状态。如果调用后返回true,必须决定响应中断还是恢复中断。

  千万不要使得中断丢失!

调用interrupt并不是意味着立即停止目标线程的工作,而只是传递了请求中断的消息。

  中断的正确理解是:发出中断请求,然后目标线程在下一个合适的时刻中断自己,当然,它也可以忽略该请求。

通常,中断是实现取消的最合理方式。
/**
* 残缺生产者的中断实现
*/
class PrimeProducer extends Thread { 
     private final BlockingQueue<BigInteger> queue; 

     PrimeProducer(BlockingQueue<BigInteger> queue) { 
         this.queue = queue; 
     } 

     public void run() { 
         try { 
             BigInteger p = BigInteger.ONE; 
            //不是一定要在循环中检测中断,但是这样能加速响应
             while (!Thread.currentThread().isInterrupted()) 
                 queue.put(p = p.nextProbablePrime()); 
             } catch (InterruptedException consumed) { 
                 /* Allow thread to exit */ 
             } 
     } 

     public void cancel() { interrupt(); } 
} 

 

 

5、中断策略

  中断策略:

    即如何处理中断,规定了线程如何解释某个中断请求。

  最合理的中断策略是线程级取消操作或服务级取消操作:

    尽快退出,必要时进行清理,告诉所有者该线程已经退出。

  任务通常不会拥有自己的线程,而是在某个服务拥有的线程中执行(如线程池),

  对于非线程拥有者的代码来说,应该小心地保存中断状态。以便拥有线程的代码对中断做出响应。

  这就是大多数可阻塞库函数的做法:

    抛出InterruptedException作为中断响应。

    因为它们永远不会在自己拥有的线程中运行,

    因此,尽快退出执行流程,并把中断消息传递给调用者,

    从而使得调用栈的上层代码能够采取进一步操作。

  检测到中断请求时,任务并不是必须放弃所有操作,它们可以推迟处理中断请求。

  除非我们明确知道中断请求的中断策略,我们的任务代码不能对线程中的中断策略有任何地猜测。

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

 

 

6、响应中断

  当调用可中断的阻塞方法时,有两种实用策略来处理中断异常:

    1、恢复中断状态,从而使上层代码能够对其进行处理。

      这在某些不想或不能抛出异常的地方有用,如使用Runnable来定义的任务。

    2、传递中断异常。从而使自己的代码也成为可中断的阻塞方法。

只有实现了线程中断策略的代码才能屏蔽中断请求。在常规的任务和库代码中都不应该这么做。

  

/**
* 在不可取消的但调用了可中断的阻塞方法(会抛出InterruptException)的任务中,必须在循环中调用可中断的阻塞方法,
* 并在发现中断后重新尝试恢复之前的运行
* 并且,应该在本地保存中断状态,并在返回前恢复中断状态(在捕获时恢复可能会引发无限循环,不断地恢复并捕获)
*/
public Task getNextTask(BlockingQueue<Taskgt; queue) { 
     boolean interrupted = false; 
     try { 
         while (true) { 
             try { 
                 return queue.take(); 
             } catch (InterruptedException e) { 
                 interrupted = true; 
                 // fall through and retry 重新尝试
             } 
         } 
     } finally { 
         if (interrupted) 
             Thread.currentThread().interrupt(); 
     } 
} 

 

  

/**
* 示例:计时运行-在外部线程中安排中断
* 在当前线程中运行任务
* 然后在ScheduledExecutorService中计时对当前线程发中断请求
* 有一个致命缺陷:当前线程必须能够响应中断,并在中断时退出。
*
* 在中断线程时,必须了解它的中断策略
*/
private static final ScheduledExecutorService cancelExec = ...; 

    public static void timedRun(Runnable r, long timeout, TimeUnit unit) { 
         final Thread taskThread = Thread.currentThread(); 
          //在ScheduledExecutorService中计时中断当前线程 
         cancelExec.schedule(    
            new Runnable() { 
             public void run() { taskThread.interrupt(); } 
         }, timeout, unit); 
         //在当前线程中运行任务
         r.run(); 
}         

 

     

/**
* 示例:计时运行-在专门的线程中中断任务
* 任务在专门的,我们自己定义了中断策略的线程中运行
* 在ScheduledExecutorService中计时中断专门的线程
*/
public static void timedRun(final Runnable r,  long timeout, TimeUnit unit) throws InterruptedException { 
     class RethrowableTask implements Runnable { 
         private volatile Throwable t;     //保存运行期间捕获的异常
         public void run() { 
             try { 
                    r.run(); 
            } catch (Throwable t) { 
                    this.t = t; 
            } 
         } 
         void rethrow() {     //重新抛出捕获到的异常
             if (t != null) 
                 throw launderThrowable(t); 
         } 
     } 
     RethrowableTask task = new RethrowableTask(); 
     final Thread taskThread = new Thread(task); 
     taskThread.start(); 
     cancelExec.schedule(
        new Runnable() { 
             public void run() { taskThread.interrupt(); } 
     }, timeout, unit); 
     taskThread.join(unit.toMillis(timeout)); 
     task.rethrow();     //重新抛出捕获到的异常
} 

 

 

7、使用Future来实现取消

  通过ExecutorService.submit 方法提交的任务将返回一个Future来描述任务。

  Future方法有cancel方法来取消任务,该方法带有一个boolean类型的mayInterruptIfRunning参数:

    参数为true:任务能够接收中断,能够在运行任务时中断线程。

    参数为false:如果任务还没有运行,那么就取消它。适合于不处理中断的任务。

  但是,除非我们了解线程的中断策略,否则都不该中断线程。

  那么什么时候mayInterruptIfRunning可以为true呢?

    执行任务的线程是由标准的Executor创建的,因为Executor的中断策略是中断则退出。

  在尝试取消任务时,不应该通过关闭线程池,因为线程池可能正在运行着多个任务。

  只能通过Future来取消线程池中的任务,正因为如此,在编写任务时,将中断视为一个取消请求。

/**
* 通过Future来实现取消
*/
public static void timedRun(Runnable r, long timeout, TimeUnit unit) throws InterruptedException { 
     Future<?> task = taskExec.submit(r); 
     try { 
            task.get(timeout, unit); 
     } catch (TimeoutException e) { 
         // task will be cancelled below 
     } catch (ExecutionException e) { 
         // exception thrown in task; rethrow 
         throw launderThrowable(e.getCause()); 
     } finally { 
         // Harmless if task already completed 
         task.cancel(true); // interrupt if running 
     } 
} 

 

当Future.get方法抛出InterruptedException或TimeoutException时,如果此时不需要结果,就可以取消任务。

 

 

8、处理不可中断的阻塞

  并非所有的阻塞方法都可以响应中断。c此时。怎么关闭它们?

    1、Java.io包中的同步Socket I/O

      关闭底层套接字,其上的阻塞方法将抛出SocketException。

    2、Java.io包中的同步I/O

      中断在一个InterruptibleChannel上等待的线程时,将抛出ClosedByInterruptException异常并关闭链路。

      导致在链路上等待的线程都抛出AsynchronousCloseException。

    3、Selector异步IO

      如果线程在调用Selector.select方法时阻塞,调用close或wakeup方法会使得线程抛出ClosedSelectorException异常,并提前返回。

    4、获取某个锁

      Lock类提供了lockInterruptibly方法,使得能够等待锁时还能响应中断。

/**
* 通过改写interrupt方法将非标准的取消操作封装在Thread中
* 即对于Socket IO上的阻塞,关闭底层Socket来实现取消
*/
public class ReaderThread extends Thread { 
     private final Socket socket; 
     private final InputStream in; 

     public ReaderThread(Socket socket) throws IOException { 
         this.socket = socket; 
         this.in = socket.getInputStream(); 
     } 
     public void interrupt() { 
         try { 
             socket.close();   //关闭底层socket来抛出异常,从而能够检测异常并退出
         } 
         catch (IOException ignored) { } 
         finally { 
             super.interrupt(); 
         } 
     } 
     public void run() { 
         try { 
             byte[] buf = new byte[BUFSZ]; 
             while (true) { 
                 int count = in.read(buf); 
                 if (count < 0) 
                     break; 
                 else if (count > 0) 
                     processBuffer(buf, count); 
             } 
         } catch (IOException e) { /* Allow thread to exit */ } 
     } 
}

 

 

/**
* 既然可以通过改写Thread的interrupt方法来实现非标准的取消操作,
* 同样,我们也可以改写Future的cancel方法来实现非标准取消操作。
* 因为interrupt方法和cancel方法都是通过向线程发消息来实现关闭指令的。
* newTaskFor是一个工厂方法,它将创建Future来表示任务。它能返回一个RunnableFuture接口
*/
public interface CancellableTask<T> extends Callable<T> { 
     void cancel(); 
     RunnableFuture<T> newTask(); 
}
 
@ThreadSafe 
public class CancellingExecutor extends ThreadPoolExecutor { 
     ... 
     protected<T> RunnableFuture<T> newTaskFor(Callable<T> callable) { 
         if (callable instanceof CancellableTask) 
             return ((CancellableTask<T>) callable).newTask(); 
         else 
             return super.newTaskFor(callable); 
     } 
} 

public abstract class SocketUsingTask<T> implements CancellableTask<T> { 
     @GuardedBy("this") 
     private Socket socket; 
     protected synchronized void setSocket(Socket s) { socket = s; } 
    //cancel被改写
     public synchronized void cancel() { 
         try { 
             if (socket != null) 
                 socket.close(); 
         } catch (IOException ignored) { } 
     } 
     public RunnableFuture<T> newTask() { 
        //这里返回了重写了cancel的Future
         return new FutureTask<T>(this) { 
             public boolean cancel(boolean mayInterruptIfRunning) { 
                 try { 
                     SocketUsingTask.this.cancel(); 
                 } finally { 
                     return super.cancel(mayInterruptIfRunning); 
                 } 
             } 
         }; 
     } 
}     

 

 

9、停止基于线程的服务

  一个服务通常会拥有多个线程(例如线程池),怎么正确地停止该服务呢?

  正确地封装原则是:除非拥有某个线程,否则不能对该线程进行操控。

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

 

/**
* 没有提供关闭方法的Logger服务
* 日志线程可以被停止(即抛出异常时)
* 但是日志服务却没有正确地被关闭
* 直接关闭(只是使日志线程退出),可能会丢失日志记录;还可能会使其他线程调用log时阻塞,因为队列此时可能是满的。。。
*/
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(); 
             } 
         } 
     } 
} 
/**
* 正确地关闭方法:在关闭时,将队列中保存的消息都记录到日志
* 采用reservations记录未写入的消息的数量
* 采用isShutDown保存关闭请求,在关闭请求为true时,停止接收日志
* 两个记录信息都要同步地被访问,因为日志服务可能被多个线程使用 * 关闭时,将未写入的消息都写入日志
*/ public class LogService { private final BlockingQueue<String> queue; private final LoggerThread loggerThread; private final PrintWriter writer; @GuardedBy("this") private boolean isShutdown; @GuardedBy("this") private int reservations; 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 (this) { if (isShutdown && reservations == 0) break; } String msg = queue.take(); synchronized (this) { --reservations; } writer.println(msg); } catch (InterruptedException e) { /* retry */ } } } finally { writer.close(); } } } }

 

 

10、停止基于线程的服务- ExecutorService

  shutdown和shutdownNow。

 

11、“毒丸” 对象

  得到毒丸时,立即停止。

  有一个缺陷,必须知道有多少个待停止的线程(要放多少个毒丸)。

public class IndexingService { 
     private static final File POISON = new File(""); 
     private final IndexerThread consumer = new IndexerThread(); 
     private final CrawlerThread producer = new CrawlerThread(); 
     private final BlockingQueue<File> queue; 
     private final FileFilter fileFilter; 
     private final File root;
 
     class CrawlerThread extends Thread { /* Listing 7.18 */ } 
     class IndexerThread extends Thread { /* Listing 7.19 */ } 

     public void start() { 
         producer.start(); 
         consumer.start(); 
     } 
     public void stop() { producer.interrupt(); } 
     public void awaitTermination() throws InterruptedException { 
         consumer.join(); 
     }  

    public class CrawlerThread extends Thread { 
         public void run() { 
             try { 
                 crawl(root); 
             } catch (InterruptedException e) { /* fall through */ } 
             finally {   //退出,放毒丸
                 while (true) { 
                     try { 
                        queue.put(POISON); 
                         break; 
                     } catch (InterruptedException e1) { /* retry */ } 
                 } 
             } 
         } 

         private void crawl(File root) throws InterruptedException { 
             ... 
         } 
    }

     public class IndexerThread extends Thread { 
         public void run() { 
         try { 
             while (true) { 
                 File file = queue.take(); 
                 if (file == POISON)   //吃到毒丸,狗带
                     break; 
                 else 
                     indexFile(file); 
             } 
         } catch (InterruptedException consumed) { } 
     } 
}   

 

 

12、shutdownNow的局限性

  shutdownNow关闭时,会返回所有已提交但是未开始的任务,但是却会丢弃已经开始执行但未结束的任务。

  可以在任务执行时,跟踪任务的执行过程,如果未执行完毕就关闭,就将它记录起来。

  以此来保留已提交但未结束的任务。

/**
* 用一个同步的set记录已提交但未完成的任务
*/
public class TrackingExecutor extends AbstractExecutorService { 
     private final ExecutorService exec; 
     private final Set<Runnable> tasksCancelledAtShutdown = Collections.synchronizedSet(new HashSet<Runnable>()); 
     ... 
     public List<Runnable> getCancelledTasks() { 
         if (!exec.isTerminated())    //没有结束时不能获取
             throw new IllegalStateException(...); 
         return new ArrayList<Runnable>(tasksCancelledAtShutdown); 
     } 

     public void execute(final Runnable runnable) { 
         exec.execute(new Runnable() { 
             public void run() { 
                 try { 
                     runnable.run(); 
                 } finally { 
                     if (isShutdown() && Thread.currentThread().isInterrupted())    //服务被关闭,但是任务没有结束
                         tasksCancelledAtShutdown.add(runnable);     //加入set
                 } 
             } 
         }); 
     } 
     // delegate other ExecutorService methods to exec 
} 

 

/**
* 网页爬虫为例,演示TrackingExecutorService的使用
* 关闭时,保存未爬取和未爬取结束的网页url 以便下次爬取时使用
*/
public abstract class WebCrawler { 
     private volatile TrackingExecutor exec; 
     @GuardedBy("this") 
     private final Set<URL> urlsToCrawl = new HashSet<URL>(); 
     ... 
     public synchronized void start() { 
         exec = new TrackingExecutor(Executors.newCachedThreadPool()); 
         for (URL url : urlsToCrawl) 
            submitCrawlTask(url); //每一个网页的爬取使一个任务
         urlsToCrawl.clear(); 
     } 

     public synchronized void stop() throws InterruptedException { 
         try { 
             saveUncrawled(exec.shutdownNow());           //保存未开始的
             if (exec.awaitTermination(TIMEOUT, UNIT)) 
                 saveUncrawled(exec.getCancelledTasks()); //保存未结束的
         } finally { 
             exec = null; 
         } 
     } 

     protected abstract List<URL> processPage(URL url); 

     private void saveUncrawled(List<Runnable> uncrawled) { 
         for (Runnable task : uncrawled) 
             urlsToCrawl.add(((CrawlTask) task).getPage()); 
     } 

     private void submitCrawlTask(URL u) { 
         exec.execute(new CrawlTask(u)); 
     } 

     private class CrawlTask implements Runnable { 
         private final URL url; 
         ... 
         public void run() { 
             for (URL link : processPage(url)) { 
                 if (Thread.currentThread().isInterrupted())  //使用interrupt作为结束消息
                     return; 
                 submitCrawlTask(link); 
             } 
         } 

         public URL getPage() { return url; } 
     } 
}

 

 

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

  程序可能会由于各种原因异常结束,异常结束时,应该通知它的上层调用者。

/**
* 典型的线程池工作者线程结构
* 捕获到异常时在本地保存异常,最后通知框架
*/
public void run() { 
     Throwable thrown = null; 
     try { 
         while (!isInterrupted()) 
         runTask(getTaskFromWorkQueue()); 
     } catch (Throwable e) { 
         thrown = e; 
     } finally { 
         threadExited(this, thrown); 
     } 
}

  Thread API中,提供了UncaughtExceptionHandler来处理线程由于未捕获的异常而终结的情况。

  如果没有注册异常处理器,默认行为是将栈信息打印到System.err。

     我们可以定义自己的处理器(通过继承Thread.UncaughtExceptionHandler),

  并将其注册给线程(通过setUncaughtExceptionHandler(Thread.UncaughtExceptionHandler eh)

/**
* 发生未捕获异常时,将其信息记录到日志
*/
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); 
     } 
} 

 

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

 

  要为线程池中所有的线程绑定同一个异常处理器,就要为线程池提供一个ThreadFactory,

  在该ThreadFactory中,为线程绑定异常处理器。

 

 

14、JVM关闭

   关闭钩子:

    在正常关闭中,JVM首先调用所有已注册的关闭钩子。

    ShutdownHook是在Runtime.addShutdownHook中注册的但未立即启动的线程。

    JVM不保证关闭钩子的启动顺序。

    如果任何应用程序线程(守护进程或非守护进程)在关闭时仍在运行,则它们将继续与关闭进程并发运行。

    当所有关闭钩子都完成时,如果runFinalizersOnExit为true,JVM可能会选择运行终结器,然后停止。

    JVM不会试图停止或中断在关机时仍在运行的任何应用程序线程;当JVM最终停止时,它们会突然终止。
  关机钩子可用于服务或应用程序清理,例如删除临时文件或清理操作系统未自动清理的资源。

/**
* 注册一个关闭钩子来停止日志服务
*/
public void start() { 
     Runtime.getRuntime().addShutdownHook(
        new Thread() { 
             public void run() { 
                 try { LogService.this.stop(); } 
                 catch (InterruptedException ignored) {} 
             } 
         }); 
}

 

 

   守护线程:

    普通线程与守护线程之间的差异仅在于退出时发生的行为。

    当JVM退出时,任何守护线程都会被立即抛弃,甚至finally代码块也不执行。

  守护线程通常不能用于管理服务的生命周期,很少有服务满足能够被突然终止的要求。

  终结器:

    finalizer

    能不用尽量不用。