一、线程Thread启动
0. Thread类实现了java.lang.Runnable接口,即实现了run方法。虽然在Sun JDK中,start()调用了start0()方法,start0()方法又是native的,但实际上新的线程就是调用了Thread的run()方法,当然这native的实现中一定有线程的fork操作,使两个线程并列执行。
1. Thread类有8个重载的构造方法。在Sun JDK的源码中,这8个构造方法都是调用了一个私有的init()方法来初始化对象的各个属性。这其中会将当前调用线程作为parent线程,确定线程组和安全权限。说一句,线程组主要是做安全方面考虑的,《Thinking in Java》这本书中表示线程组实际上是一个比较失败的东西,没有具体去提,本文也不会详细去说明。除此之外的代码逻辑如下,设置了各个Thread对象的属性。
private void init(ThreadGroup g, Runnable target, String name, long stackSize) { if (name == null) { throw new NullPointerException("name cannot be null"); } Thread parent = currentThread(); SecurityManager security = System.getSecurityManager(); if (g == null) { //Determine if it‘s an applet or not // If there is a security manager, ask the security manager what to do. if (security != null) { g = security.getThreadGroup(); } //If the security doesn‘t have a strong opinion of the matter use the parent thread group. if (g == null) { g = parent.getThreadGroup(); } } // checkAccess regardless of whether or not threadgroup is explicitly passed in. g.checkAccess(); // Do we have the required permissions? if (security != null) { if (isCCLOverridden(getClass())) { security.checkPermission(SUBCLASS_IMPLEMENTATION_PERMISSION); } } g.addUnstarted(); this.group = g; this.daemon = parent.isDaemon(); this.priority = parent.getPriority(); this.name = name.toCharArray(); if (security == null || isCCLOverridden(parent.getClass())) this.contextClassLoader = parent.getContextClassLoader(); else this.contextClassLoader = parent.contextClassLoader; this.inheritedAccessControlContext = AccessController.getContext(); this.target = target; setPriority(priority); if (parent.inheritableThreadLocals != null) this.inheritableThreadLocals = ThreadLocal.createInheritedMap(parent.inheritableThreadLocals); // Stash the specified stack size in case the VM cares this.stackSize = stackSize; // Set thread ID tid = nextThreadID(); }
其中target属性是Runnable对象,也是线程所要完成的执行任务内容,而deamon则是一个是否为守护线程的布尔值标记,可以看到默认是继承于当前调用线程的(parent),这个可以通过setDaemon来重新设置,上下文的类加载器contextClassLoader也是一样。
2. 线程的运行启动。即调用start()方法,上面刚刚提到,实际会调用到run()方法。如果是Thread的子类,则可以通过重写run()方法来做任务实现。而在Thread默认的run()方法实现中,是这样子的:
if (target != null) { target.run(); }
这个target前面提到过,就是初始化的Runnable任务对象。
3. 线程状态。对于一个线程来讲,状态通常可以分为几大类NEW、RUNNING、BLOCKED、DEAD,当然Running可以分为真正执行和就绪,阻塞BLOCKED也有多种阻塞方式。在1.5之后的Sun JDK中,Thread内部是有一个State的枚举类的,但这个通常不是用来做开发,而是用于测试。通常来说,我们能做的就是用Thread对象的isAlive()方法判断,得到true或者false。
二、线程Thread终止
看下面代码,觉得会怎样?
public class App { static class ExRun implements Runnable { public void run() { System.out.println("throwing..."); throw new RuntimeException(); } } public static void main(String[] args) { try { new Thread(new ExRun()).start(); } catch (Exception e) { System.out.println("runtime ex. catched!"); } } }
会在控制台打出”runtime ex. catched!”?你确定么?不怕大家笑话,几年我第一眼看类似这段代码的时候,竟然也认为这很理所当然。其实,这也许不是年头的问题,是是否仔细思考和经验问题。只要仔细想一想,既然已经new了一个Thread并且让他start()去了,异常又不是在new的过程中或者start的过程中出来的,抛不抛异常关这个主线程什么事儿。
那如果一个被主线程创建的新线程除了什么“故障”,就让其悄悄地自生自灭么?有没有补救方案呢?有的。在JavaSE5之前,这个问题直接交给线程组(ThreadGroup)了事儿,但在这之后就有了java.lang.Thread$UncaughtExceptionHandler($是内部关系,后者是前者的内部类/接口)这么一个interface,里面只有一个方法:
void uncaughtException(Thread t, Throwable e);
这个方法就是负责处理异常的,Thread参数不说了,那这个Throwable又是啥,这个就是Exception的老爸了,不了解的异常类层次结构的,这儿您就可以查查去了。既然是Throwable,其实也就不只是Exception了,Error也会被处理到。
接着就简单说下怎么用,Thread类的两个方法:
- public void setUncaughtExceptionHandler(UncaughtExceptionHandler eh)
- public static void setDefaultUncaughtExceptionHandler(UncaughtExceptionHandler eh)
第一个是实例方法,需要针对某个Thread对象使用,而第二个是静态方法,设置全局的默认处理方法,但这两个不冲突,在Thread中是两个独立的handler属性。但毕竟默认的和非默认的还不一样,有个顺序问题,这里又不得不再提到线程组(ThreadGroup)。
在JavaSE5之前,没有UncaughtExceptionHandler这个东西,所以出错会直接交给Thread的ThreadGroup对象属性group处理,因为ThreadGroup有处理异常的方法,而UncaughtExceptionHandler出来后,ThreadGroup也实现了这个接口。按照文档注释说明:
- 优先检查线程的uncaughtExceptionHandler
- 否则交给group处理
- 最后才可能轮到defaultUncaughtExceptionHandler
而看源码中线程组的处理实际上是追溯一个parent(也是ThreadGroup类对象)链:
public void uncaughtException(Thread t, Throwable e) { if (parent != null) { parent.uncaughtException(t, e); } else { Thread.UncaughtExceptionHandler ueh = Thread.getDefaultUncaughtExceptionHandler(); if (ueh != null) { ueh.uncaughtException(t, e); } else if (!(e instanceof ThreadDeath)) { System.err.print("Exception in thread \"" + t.getName() + "\" "); e.printStackTrace(System.err); } } }
最后为空时,采用全局默认的的handler。
线程异常就说道这,已经说了不少了,下面简单说两句JVM的退出/停止。
通常我们在课本上都会知道java.lang.System类有个方法是exit(int),这个会让程序退出,而实际上这个方法实际上是java.lang.Runtime类exit(int)的封装(其实System还有像gc()等很多方法,实际执行者都是这个java.lang.Runtime类对象),意义是告诉当前运行环境关闭运行,也就通常被理解为JVM“关闭”。
说下Runtime类,这个类是个单例模式的类,即从头到尾程序只能获得同一个对象引用,即调用静态方法Runtime.getRuntime()所得。Runtime类负责和运行时环境相关的各种任务,有很多有趣的方法,比如很多应用(如Tomcat)中某些线程池中线程数目的确定和当前运行环境处理器个数有关,则可通过Runtime的availableProcessors()方法获得。这里重点说下exit()方法。
还有一个概念需要说下,就是ShutdownHook,这个听起来“高端洋气上档次”的东西实际上就是一个Thread,但它的特殊就在于是一个hook,在JVM关闭(exit)的时候才会执行到。就如线程和中断的关系一样,JVM和exit()也是一样,exit()的执行并不是虚拟机马上关掉,而是安排结束时所要执行任务,按文档说明分上下两个过程,前面的过程就是shutdown对应Thread执行的过程。要想让虚拟机优雅的结束,exit()安排执行shutdownHook清理环境是必要的,还说Tomcat吧,终止的时候也会执行注册过的hook。执行hook的时候,通常守护/非守护线程也都仍然可以执行,但对hookd的新增和移除则不可以了。
Runtime和shutdonwHook关联起来的方法,主要就是这俩了:
- public void addShutdownHook(Thread hook)
- public boolean removeShutdownHook(Thread hook)
具体用法不用多说估计看文档都能懂,需要注意的地方就是,想要remove必需在add的时候留有hook的Thread对象引用。
和Thread的stop()类似,Runtime也有halt()方法,但这是一个“暴力停机”的方法,不会考虑hook的执行,如果情况允许,exit()还是优雅的选择。
转自:
线程、Thread类和线程终止相关整理(下)——线程异常&JVM停止