处理非正常的线程终止
导致线程提前死亡的最主要原因就是RuntimeException,由于这些异常表示出现了某种编程错误或者其他不可修复的错误,因此它们通常不会被捕获.它们不会在调用栈逐层传递,而是默认地在控制台中输出栈追踪信息,并终止线程.
在并发程序中,是无法做到一直观察控制台的, 例如:你的web应用部署到服务器上,难道你要派个人一直去观察控制台?
线程非正常退出的后果可能是良性的,也可能是恶性的,这要取决于线程在应用程序中的作用.
虽然在线程池中丢失一个线程可能会对性能带来一定的影响,但如果程序能在包含50个线程的线程池上运行良好,那么在包含49个线程的线程池上通常也能运行良好.
然而,如果在GUI程序中丢失了事件分派线程,那么造成的影响僵尸非常显著的–应用程序将停止处理事件,并且GUI会因此而失去响应.
任何代码都可能抛出一个RuntimeExecption,每当调用另一个方法时,都要对它的行为保持怀疑,不要盲目地认为它一定会抛出在方法原型中声明的某个已检查异常.对调用的代码越不熟悉,就越应该对其代码行为保持怀疑
典型的线程池工作者线程结构
public void run(){
Throwable throw = null;
try{
while(!isInterrupted){
runTask(getTaskFromWorkQueue());
}
} catch (Throwable e){
thrown = e;
} finally{
threadExited(this,thrown);
}
}
如果任务抛出了一个未检查的异常,那么它将使线程终结,但会首先通知框架该线程已经终结.然后,框架可能会用新的线程来代替这个工作线程.
将异常写入日志的UncaughtExecptionHandler
public class Thread.UncaughtExecptionHandler{
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或者在操作系统中杀死JVM进程).
关闭钩子
在正常关闭中,JVM首先调用所有已注册的关闭钩子,关闭钩子是指通过Runtime.addShutdownHook注册的但尚未开始的线程.
JVM不能保证关闭钩子的调用顺序.在关闭应用程序线程时,如果有线程仍然在运行,那么这些线程接下来将与关闭进程并发执行.
关闭钩子应该是线程安全: 它们在访问共享数据时,必须使用同步机制,小心避免死锁.
关闭钩子最好串行执行!!!
守护线程
有时候,你希望创建一个线程来执行一些辅助工作,但又不希望这个线程阻碍了JVM的关闭,这种情况就需要使用守护线程.
线程分为两种: 普通线程和守护线程,在JVM启动时启动创建的所有线程中,除了主线程以外,其他的线程都是守护线程,例如垃圾回收器,当创建一个新的线程时,它将继承创建它的线程的类型.
守护线程:
我们应该尽可能少地使用守护线程–很少有操作能够在不进行清理的情况下被安全地抛弃,特别是在执行I/O操作的任务,那么将是一种非常危险的行为; 并且守护线程不能用来替代应用程序管理程序中各个服务的生命周期
终结器
当不再需要内存资源时,可以通过垃圾回收器来回收它们,但对于其他一些资源,例如文件句柄或套接字句柄,当不再需要它们时,必须显式交还给操作系统.为了实现这个功能,垃圾回收器对那些定义了finalize方法的对象会进行特殊处理: 在垃圾回收期释放它们后,调用它们的finalize方法,从而保证一些持久化的资源被释放.
由于终结器可以在某个JVM管理的线程中运行,因此终结器访问任何状态都可能被多个线程访问,这样就必须对其访问操作进行同步.
终结器并不能保证它们将在何时甚至是否会运行,并且复杂的终结器带来性能上的巨大开销.编写正确的终结器是非常困难的.
在大多数情况下,通过使用finally代码块和显式的close方法能够比终结器更好的管理资源.
避免使用终结器