取消与关闭

Java没有提供任何机制来安全地终止线程。但提供了中断,这是一种协作机制,能够使一个线程终止另一个线程的当前工作。

一个在行为良好的软件与勉强运行的软件之间的最主要区别就是,行为良好的软件能很完善地处理失败、关闭和取消等过程。

任务取消

如果外部代码能够在某个操作正常完成之前将其置入“完成”状态,那么这个操作就可以成为可取消的,取消的原因很多:

  • 用户请求取消
  • 有时间限制的操作
  • 应用程序事件
  • 出现错误
  • 程序关闭

在Java中没有一种安全的抢占式方法来停止线程,因此也就没有安全的抢占式方法来停止任务。只有一些协作式的机制,使请求取消的任务和代码都遵循一种协商好的协议。

其中一种协作式的机制能设置某个“已请求取消”标志(在Runnable中定义),而任务将定期地查看该标志。如果设置了这个标志,那么任务将提前结束。

一个可取消的任务必须拥有取消策略,在这个策略中将详细地定义取消操作的How、When以及What,即其他代码如何(how)请求取消该任务,任务在何时(when)检查已经请求了取消,以及在响应取消请求时应该执行哪些(what)操作。

中断

上面的策略是有一个问题的,如果任务在做BlockingQueue的put操作时阻塞,那么它不可能及时检查取消状态。

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

每个线程都有一个布尔类型的中断状态。当中断线程时,这个状态被设为true。在Thread中包含了中断线程以及查询线程中断状态的方法(isInterrupted)。interrupt方法能中断目标线程,而静态的Interrupted方法将清除当前线程的中断状态,并返回它之前的值,这也是清除线程状态的唯一方法。

阻塞库方法,例如Thread.sleep和Object.wait等,都会检查线程何时中断,并且在发现中断时提前返回。它们在响应中断时执行的操作包括:清除中断状态,抛出InterruptedException,表示阻塞操作由于中断而提前结束。JVM并不能保证阻塞方法检测到中断的速度,但在实际情况中响应速度还是非常快的。

当线程在非阻塞状态下中断时,它的中断状态将被设置,然后根据将被取消的操作来检查中断状态以判断发生了中断。通过这样的方法,中断操作将变得“有黏性”===如果不触发InterruptedException,那么中断状态将一直保持,直到明确地清除中断状态。调用Interrupt并不意味着立即停止目标线程正在进行的工作,而只是传递了请求中断的消息。

这里可以看出中断是实现取消的最好方式。如果设置了中断标志,那么线程在在执行BlockingQueue的put操作时阻塞,这样就会触发InterruptedException,然后我们在关闭这个线程。

中断策略

注意这里的中断策略指的是当前线程去中断目标线程的策略,当前线程中可能有设置的标记。后面的要在这个基础上理解,否则看不明白!

正如取消策略一样,线程应该包含中断策略。中断策略规定线程如何解释某个中断请求----当发现中断请求时,应该做哪些工作(如果需要的话),那些工作单元对于中断来说是原子操作,以及以多快的速度来响应中断。

最合理的中断策略是某种形式的线程级取消操作或服务级的取消操作:尽快退出,在必要的时候进行清理,通知某个所有者线程已经退出。此外还可以建立其他的中断策略,例如暂停服务或重新开始服务,但对于那些包含非标准中断策略的线程或线程池,只能用于能知道这些策略的任务中。

区分任务和线程对中断的反应是很重要的。一个中断请求可以有一个或多个接收者----中断线程池中的某个工作者线程,同时意味着“取消当前任务”和“关闭工作者线程”。

任务不会在其自己拥有的线程中执行,而是在某个任务(例如线程池)拥有的线程中执行。对于非线程所有者的代码来说(例如,对于线程池而言,任何在线程池实现以外的代码),应该小心地保存中断状态,这样拥有线程的代码才能对中断作出响应,即使“非所有者”代码也可以做出响应。

这是为啥大多数可阻塞的库函数都只是抛出InterruptedException作为终端响应。它们永远不会在某个由自己拥有的线程中运行,因此它们为任务或库代码实现了最合理的取消策略:尽快退出执行流程,并把中断消息传递给调用者,从而使得调用栈中的上层代码可以采取进一步的操作。

当检查到中断请求时,任务并不需要放弃所有的操作----它可以推迟处理中断请求,并直到某个更合适的时刻。因此需要记住中断请求,并在完成当前任务后抛出InterruptedException或者表示已收到中断请求。这项技术能够确保在更新过程中发生中断时,数据结构不会被破坏。

任务不应该对执行该任务的中断策略做出任何假设,除非该任务被专门设计为在服务中运行,并且在这些服务中包含特定的中断策略。无论任务把中断视为取消,还是其他某个中断响应操作,都应该小心地保存执行线程的中断状态。如果除了将InterruptedException传递给调用者外还需要执行其他操作,那么应该在捕获InterruptedException之后恢复中断状态:Thread.currentThread().interrupt();

正如任务代码不应该对其执行所在的线程的中断策略做出假设,执行取消操作的代码也不应该对线程的中断策略做出假设。线程应该只能由其所有者中断,所有者可以将线程的中断策略信息封装到某个合适的取消机制中,例如关闭(shutdown)方法。

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

响应中断

当线程处于中断状态,在调用Thread.sleep或BlockingQueue.put等方法时阻塞时,出现InterruptedException异常,对这个异常的处理有两种方式:

  • 传递异常(可能在执行某个特定的任务清除操作之后),从而使你的方法也成为可中断的阻塞方法。
  • 恢复中断状态,从而使调用栈中的上层代码能够对其进行处理。

通过Future来实现取消

我们已经使用了一种抽象机制来管理任务的生命周期,处理异常,以及实现取消,即Future。

当Future.get抛出InterruptedException和TimeoutException时,如果你知道不再需要结果,那么就可以调用Future.cancel来取消任务。cancel方法带一个boolean参数mayInterruptIfRunning参数,表示取消操作是否成功。如果为true并且当前线程处于运行状态,那么这个线程会被中断。如果为false,那么意味着若线程还没启动就不要运行它,这种方式应该用于那些不处理中断的任务中。

通过Future来取消任务

处理不可中断的阻塞

在java类库中,许多可阻塞的方法都是通过提前返回或者抛出InterruptedException来响应中断请求的,从而使开发人员更容易构建出能响应取消请求的任务。然而,并非所有的可阻塞方法或者阻塞机制都能响应中断;如果一个线程由于执行同步的Socket I/O或者等待获得内置锁而阻塞,那么中断请求只能设置线程的中断状态,除此之外没有其他任何作用。对于那些由于执行不可中断操作而被阻塞的线程,可以使用类似于中断的手段来停止这些线程,但这要求我们必须知道线程阻塞的原因。

  • Java.io包中的同步Socket I/O。可以通过关闭底层的套接字,来使线程响应中断。
  • java.io包中的同步I/O。当中断一个正在InterruptibleChannel上等待的线程时,将抛出ClosedByInterruptException并关闭链路(这还会使得其他在这条链路上阻塞的线程同样抛出ClosedByInterruptException)。当关闭一个InterruptibleChannel时,将导致所有在链路操作上阻塞的线程都抛出AsynchronousCloseException。大多数标准的Channel都实现了InterruptibleChannel。
  • Selector的异步I/O。如果一个线程在调用Selector.select方法(在java.nio.channels中)时阻塞了,那么调用close或wakeup方法会使线程抛出ClosedSelectorException并提前返回。
  • 获取某个锁。如果一个线程由于等待某个内置锁而阻塞,那么将将无法响应中断,因为线程认为它肯定会获得锁,所以将不会理会中断请求。但是,在Lock类中提供了lockInterruptibly方法,该方法允许在等待一个锁的同时仍能响应中断。

以上的策略中,你可以自己定义一个线程,然后改写interrupt方法,比如在socket连接中阻塞,可以在Interrupt方法中加入socket.close调用,然后调用super.interrupt。注意应该将super.intertupt放在finally语句块中。

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

我们可以通过newTaskFor方法进一步优化ReaderThread中封装非标准取消的技术,这是java6在ThreadPoolExecutor中新增功能。当把一个Callable提交给ExecutorService时,submit方法返回一个Future,我们可以通过这个Future来取消任务。newTaskFor是一个工厂方法,它将创建Future来代表任务。newTaskFor还能返回一个RunnableFuture接口,该接口扩展了Future和Runnable(并由FutureTask实现)。

通过定制表示任务的Future可以改变Future.cancel的行为。例如,定制的取消代码可以实现日志记录或者收集取消操作的统计信息,以及取消一些不响应中断的操作。通过改写Interrupt方法,ReaderThread可以取消基于套接字的线程。同样,通过改写任务的Future.cancel方法也可以实现类似的功能。

这个具体例子可以查看: Java 并发编程之任务取消(四)

停止基于线程的服务

正确的封装原则是:除非拥有某个线程,否则不能对该线程进行操控。例如中断线程或修改线程的优先级等 。那么什么 是拥有某个线程呢,就是创建该线程的类,一般来说线程池是其工作线程的所有者,所以要修改线程的话,需要使用线程池来执行.

线程的所有权是不能传递的。在ExecutorService中提供了shutdown和shutdownnow等方法,同样,在其他拥有线程的服务中也应该提供类似的关闭机制。

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

日志服务的例子

在该例中,写一个LogWriter类,里面有BlockingQueue和LoggerThread两个属性,LoggerThread是一个写日志的线程,数据来源于BlockingQueue。

关闭有两种方式,一是直接退出LoggerThread,显然这是不合理的,因为这会导致队列中有log没写入,同时也会阻塞put的线程。不是一种完备的关闭方式。

另一种策略是设置一个“已请求关闭”的标志,以避免进一步的日志提交,同时处理完队列中的log。但是这种是“先判断再执行”的代码序列,同样会存在阻塞的问题。因此此时要使日志的提交变为原子操作。

停止基于线程的服务

关闭ExecutorService

ExecutorService提供了两种关闭方法,使用Shutdown正常关闭,以及使用ShutdownNow强行关闭。在进行强行关闭时,shutdownNow首先关闭当前正在执行的任务。然后返回所有尚未启动的任务清单

在复杂的程序中,通常会将ExecutorService封装在某个更高级别的服务中,并且该服务能提供其自己的生命周期方法。

“毒丸”对象

毒丸是指一个放在队列上的对象 ,其作用是当得到这个对象的时候,立即停止。在FIFO队列中,毒丸对象将确保消费者在关闭之前首先完成队列中的所有工作。在提交毒丸对象之前的所有工作会被处理,提交毒丸之后的任何任务,将不会被处理。

只有在生产者和消费者的数量都已知的情况下,才可以使用“毒丸”对象。当生产者多的时候 ,可以加一个计数器,当所有生产者的丸子都放在队列里边的时候再进行打断。多消费者。的时候 ,一个生产者可以放入与消费者数量相同的丸子。因为每个消费者都只能接收一个丸子。当两者数量都比较大时就不太好用了。只有在无界队列中。毒丸对象才能可靠的工作。

关闭ExecutorService

只执行一次的服务

如果某个方法需要处理一批任务,并且当所有任务都处理完成之后才返回,那么可以通过一个私有的Executor来简化服务的生命周期管理,其中该Executor的生命周期是由这个方法来控制的。(在这种情况下,invokeAll和invokeAny等方法通常会起较大的作用)

boolean checkMain(Set<String> hosts, long timeout, TimeUnit unit) throws InterruptedException {  
    ExecutorService exec = Executors.newCachedThreadPool();  
    final AtomicBoolean hasNewMail = new AtomicBoolean(false);  
    try {  
        for (String host : hosts) {  
            exec.execute(new Runnable() {  

                @Override  
                public void run() {  
                    // TODO Auto-generated method stub  
                    if (checkMail(host)) {  
                        hasNewMail.set(true);  
                    }  
                }  
            });  
        }  
    } finally {  
        exec.shutdown();  
        exec.awaitTermination(timeout, unit);  
    }  
    return hasNewMail.get();  
}

shutdownNow的局限性

上次说shutdownNow会返回所有尚未启动的Runnable。但是它无法返回的是正在执行的Runnable。通过封装ExecutorService并使得execute记录哪些任务是在关闭后取消的。并且在任务返回时必须保持线程的中断状态。

所以利用这个特性我们可以在线程的Run方法中判断当前线程是否是正在运行被关闭,可以在final块中去判读,当前的线程是不是中断状态,是的话加入中断的List列表中。

@Override  
public void execute(final Runnable command) {  
    // TODO Auto-generated method stub  
    exec.execute(new Runnable() {  
        public void run() {  
            try {  
                command.run();  
            } finally {  
                if (isShutdown() && Thread.currentThread().isInterrupted()) {  
                    tasksCancelledAtShutdown.add(command);  
                }  
            }  
        }  
    });  

}

只运行一次的服务

处理非正常的线程终止

当单线程的控制台程序由于 发生了一个未捕获的异常而终止时,程序将停止运行,并产生与程序正常输出非常不同的栈追踪信息,这种情况是很容易理解的。然而,如果并发程序中的某个线程发生故障,那么通常不会如此明显。在控制台中可能会输出栈追踪信息,但没有人会观察控制台。此外,当线程发生故障时,应用程序可能看起来仍然 在工作,所以这个失败很可能被忽略。下面要讲的问题就是监测并防止在程序中“遗漏”线程的方法 。

导致线程提前死亡的最主要原因就是RuntimeException。

我们可以在线程池内部构建一个工作者线程。如果任务抛出一个未检查异常,那么它将使线程终结。框架可能会用新的线程代替这个工作线程,也可能不会。因为线程池正在关闭,或者 已经有足够多的线程满足需要。

当编写一个向线程池提交任务的工作者线程类时,或者调用不可信的外部代码时(例如动态加载的插件)。使用这些方法中的某一种可以避免某个编写得糟糕的任务或插件不会影响调用它的整个线程。

通常像下面这样写就可以了:

public void run() {  
    Throwable thrown = null;  
    try {  
        while (!isInterrupted())  
            runTask(getTaskFromWorkQueue());  
    } catch (Throwable e) {  
        thrown = e;  
    } finally {  
        threadExited(this, thrown);  
    }  
}

未捕获异常的处理

上面是一种主动方法来解决未检查异常,在Thread API中同样提供了UncaughtExceptionHandler,它能检测出来 某个纯种由于未捕获异常而终结的情况。这两种情况是互补的。通过将两者结合在一起,能有效地防止线程泄漏的问题

当一个线程由于未捕获异常而退出时,JVM会把这个事件报告给应用程序提供的UncaughtExceptionHandler异常处理器。如果没有提供任何异常处理器。那么默认的行为是将栈追踪信息输出到System.err。

public class TestThread extends Thread {  

    @Override  
    public void setUncaughtExceptionHandler(UncaughtExceptionHandler eh) {  
        // TODO Auto-generated method stub  
        super.setUncaughtExceptionHandler(new UncaughtExceptionHandler() {  

            @Override  
            public void uncaughtException(Thread t, Throwable e) {  
                // TODO Auto-generated method stub  

            }  
        });  
    }  

}

最常见的响应方式是将一个错误信息以及相应的栈追踪信息写入应用程序日志中。还可以采用更直接的响应。例如尝试重新启动线程,关闭应用程序,或者执行其他修复或诊断等操作。

public class UEHLogger implements Thread.UncaughtExceptionHandler {  

    @Override  
    public void uncaughtException(Thread t, Throwable e) {  
        // TODO Auto-generated method stub  
        Logger logger = Logger.getAnonymousLogger();  
        logger.log(Level.SEVERE,"thread terminted with exception" + t.getName(), e);  
    }  

}

要为线程池中的所有线程设置一个UncaughtExceptionHandler。需要为ThreadPoolExecutor的构造函数提供一个ThreadFactory。(只有线程的所有者能够改变线程的UncaughtExceptionHandler。)标准线程池允许当发生未捕获异常时结束线程,但由于使用了一个try-finally代码块来接收通知,因此当线程结束时,将有新的线程来代替他。如果没有提供捕获异常处理器或者其他的故障通知机制,那么任务会悄悄失败,从而导致极大的混乱。如果你希望在任务由于发生异常而失败时获得通知,并且执行一些特定于任务的恢复操作,那么可以将任务封装在能捕获异常的Runnable或者Callable中,或者改写ThreadPoolExecutor的afterExecute方法。

令人困惑的是,只有通过execute提交的任务,才能将它抛出的异常交给未捕获异常处理器,而通过submit提交的任务,无论是抛出的未检查异常还是已检查异常,都将被认为是任务返回状态的一部分。如果一个由submit提交的任务由于抛出异常而结束,那么这个异常将被Future.get封装在ExecutionException中重新抛出。

处理非正常的线程中止

JVM关闭

jvm可正常关闭也可强行关闭,正常关闭有多种触发方式:

  • 当最后一个正常(非守护,下面会讲到什么是守护线程)线程结束时
  • 当调用system.exit时,或者通过其他特定于平台的方法关闭时(例如发送了SIGINT信号或键入Ctrl-c)
  • 通过其他特定平台的方法关闭jvm,调用Runtime.halt或者在操作系统当中杀死JVM进程(例如发送sigkill)来强行关闭jvm。

关闭钩子

在正常关闭中,jvm首先调用所有已注册的关闭钩子,关闭钩子是指通过 Runtime.addShutdownHook注册的但尚未开始的线程。JVM并不能保证关闭钩子的调用顺序。在关闭应用程序线程时,是并发执行的。

当所有关闭钩子都执行结束时,如果runFinalizersOnExit为true,那么jvm将运行终结器,然后再停止。jvm并不会停止或中断任何在关闭时仍然运行的应用程序线程。

当jvm最终结束时,这些线程将被强行结束。如果关闭钩子或终结器没有执行完成,那么正常关闭进程挂起并且 jvm必须强行关闭。当被强行关闭时,只是关闭jvm。而不会运行关闭钩子。

关闭钩子应该是线程安全的,它们在访问共享数据时必须使用同步机制,并且避免发生死锁。

而且关闭钩子不应该对应用程序的状态(如:其它服务是否已经关闭,或者 所有正常线程是否已经执行完成)或者对jvm的关闭原因做出假设。因此在编写关闭钩子的代码时必须考虑周全。最后,在关闭钩子时应该尽快退出,因为他们会延迟jvm的结束时间,而用户希望JVM能尽快终止。

守护进程

守护线程其实就是辅助线程。比如在jvm启动时创建的所有线程中,除了主线程以外,其他的线程都是守护线程(例如垃圾回收器)。默认情况下,由主线程创建的所有线程都是普通线程。同理,由守护线程创建的线程也全是守护 线程,线程之间存在继承关系。

守护线程和普通线程之间的区别仅在于线程退出时发生的操作:

当一个线程退出时,jvm会检查其他正在运行的线程,如果 是守护线程,jvm会正常退出。当jvm停止时 ,所有仍然存在的守护线程都将被抛弃。不会执行finally块,也不会回卷栈。只是直接退出。

终结器

对于一些资源,如文件句柄或者套接字句柄,当不再需要他们时,必须显式的交还给操作系统,垃圾回收器会那些定义了finalize方法的对象进行特殊处理。当回收它们时会调用他们的finalize方法 。复杂的终结器会产生巨大的开销,所以应该避免使用终结器。

results matching ""

    No results matching ""