ShutdownHookManager的设计和jdk包中的设计基本类似,所以很有必要先分析一波jdk中的shutdownHook的源码,具体如下。
在Runtime.java
这个类中,有一个方法addShutdownHook
,这个方法的作用就是允许用户注册一些JVM异常关闭的处理代码。先说明一下,JVM的关闭退出分为两类:
1> 正常的退出,例如程序运行完毕正常的自动退出、程序主动调用了System.exit(int)
方法;
2> 用户强制关闭应用,例如手动执行kill pid
(kill -9 pid
不会触发)或是ctrl+C
这种方式而导致JVM关闭。
而我们这里说的addShutdownHook
方法,就是针对第二种关闭情况下,虚拟机进行的收尾操作,例如某个应用在执行过程中,产生了一些中间文件,执行着执行着,突然应用被强行kill了,此时假设调用过addShutdownHook
注册了收尾的线程对这些中间文件执行清理操作,那么JVM会启动这个线程去清理这些临时文件然后退出。当然一个JVM运行过程中,其实是可以注册多个这类线程做不同类型的收尾善后工作的,查看addShutdownHook
方法如下:
public void addShutdownHook(Thread hook) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new RuntimePermission("shutdownHooks"));
}
ApplicationShutdownHooks.add(hook);
}
这里的入参是一个Runnable
的任务,其中可以设置线程优先级,对于JVM而言它在运行这些hook线程的时候,是遍历后依次启动,执行顺序是完全无法保障的。
另外注意到addShutdownHook
是通过调用ApplicationShutdownHooks.add(hook)
方法来进行hook线程注册的,我们来看一下这个ApplicationShutdownHooks
类的结构:
class ApplicationShutdownHooks {
private static IdentityHashMap<Thread, Thread> hooks;
static {
try {
Shutdown.add(1 /* shutdown hook invocation order */,
false /* not registered if shutdown in progress */,
new Runnable() {
public void run() {
runHooks();
}
}
);
hooks = new IdentityHashMap<>();
} catch (IllegalStateException e) {
// application shutdown hooks cannot be added if
// shutdown is in progress.
hooks = null;
}
}
private ApplicationShutdownHooks() {}
static synchronized void add(Thread hook) {
if(hooks == null)
throw new IllegalStateException("Shutdown in progress");
if (hook.isAlive())
throw new IllegalArgumentException("Hook already running");
if (hooks.containsKey(hook))
throw new IllegalArgumentException("Hook previously registered");
hooks.put(hook, hook);
}
static synchronized boolean remove(Thread hook) {
if(hooks == null)
throw new IllegalStateException("Shutdown in progress");
if (hook == null)
throw new NullPointerException();
return hooks.remove(hook) != null;
}
static void runHooks() {
Collection<Thread> threads;
synchronized(ApplicationShutdownHooks.class) {
threads = hooks.keySet();
hooks = null;
}
for (Thread hook : threads) {
hook.start();
}
for (Thread hook : threads) {
try {
hook.join();
} catch (InterruptedException x) { }
}
注意这个类中的构造函数是私有化的,即外界是无法实例化这个对象的,而且它的所有方法均是静态的,那么这个类的作用很显然就是提供给用户进行hook线程注册的统一入口,本应用中所有需要注册hook线程的地方,均只能通过调用ApplicationShutdownHooks.add(hook);
方法来进行注册。
由ApplicationShutdownHooks
的构造可以发现,在用户首次注册hook线程时,会调用静态代码块中的方法,到Shutdown
类中去注册一个启动所有用户所注册的hook线程的父线程任务,还可以注意到Shutdown.add(int,boolean,Runnable)
方法,其代码如下:
static void add(int slot, boolean registerShutdownInProgress, Runnable hook) {
synchronized (lock) {
if (hooks[slot] != null)
throw new InternalError("Shutdown hook at slot " + slot + " already registered");
if (!registerShutdownInProgress) {
if (state > RUNNING)
throw new IllegalStateException("Shutdown in progress");
} else {
if (state > HOOKS || (state == HOOKS && slot <= currentRunningHook))
throw new IllegalStateException("Shutdown in progress");
}
hooks[slot] = hook;
}
}
这其中的hooks是一个线程数组:Runnable[] hooks = new Runnable[MAX_SYSTEM_HOOKS]
而很显然slot就是对应注册的启动hook线程的父线程在个数组中的位置,这个位置的左右就是在接下来启动这些线程的时候,启动顺序就是按照数组进行遍历。由于ApplicationShutdownHooks
中注册的时候插入的数组中的位置slot=1
,那么对应数组中的第一个位置是被谁占据了呢?对Shutdown.add()
方法,通过用Intellij IDEA的find usage
可以发现,另外一个调用这个方法的地方在System
这个类中,进行查看可以发现是在System
类初始化的时候就会调用一个setJavaLangAccess()
的方法,在这个方法中会把Shutdown
中的hooks
数组中的第一个坑位给霸占了!
另外,依据Runtime.java
类中的备注来看,在使用addShutdownHook
注册hook线程做善后工作时,还是需要注意尽量要避免线程内产生死锁,而且由于执行了exit
方法后要尽量快的关闭应用,所以不要在这些线程中执行一些十分费时的操作(当然你如果要耍流氓好像也没啥办法吧?);还有就是,如果某个线程已经注册过,那么再次注册会报错,不过这里因为保存用户注册hook线程的是一个特殊的IdentityHashMap
,这个map判断两个key是否相同的条件是key1=key2
即直接判断两个对象的引用是否相同,意思就是对于同一个hook线程,不能连续两次调用注册,否则会报错。
以上主要是jdk中的shutdownHook
的主要逻辑,注意已经说过jdk中的逻辑完全无法保障线程的执行顺序,但是在hadoop中的ShutdownHookManager
中,它在启动这些hook线程前,会对这个对应的hook线程数组进行排序,代码如下:
List<Runnable> getShutdownHooksInOrder() {
List<HookEntry> list;
synchronized (MGR.hooks) {
list = new ArrayList<HookEntry>(MGR.hooks);
}
Collections.sort(list, new Comparator<HookEntry>() {
//reversing comparison so highest priority hooks are first
@Override
public int compare(HookEntry o1, HookEntry o2) {
return o2.priority - o1.priority;
}
});
List<Runnable> ordered = new ArrayList<Runnable>();
for (HookEntry entry: list) {
ordered.add(entry.hook);
}
return ordered;
}
其中HookEntry为封装的Runnable任务,结构如下:
private static class HookEntry {
Runnable hook;
int priority;
public HookEntry(Runnable hook, int priority) {
this.hook = hook;
this.priority = priority;
}
@Override
public int hashCode() {
return hook.hashCode();
}
@Override
public boolean equals(Object obj) {
boolean eq = false;
if (obj != null) {
if (obj instanceof HookEntry) {
eq = (hook == ((HookEntry)obj).hook);
}
}
return eq;
}
}
因此,在这里通过优先级先对数组进行排序,然后可以保证优先级高的数组优先启动,这样可以尽量保证优先级高的线程先一步执行完成(这个顺序肯定不是一定的,和系统调度有关),其它逻辑和jdk的实现基本一致,不再赘述。