來聊聊守護(hù)線程和 JVM 的優(yōu)雅關(guān)閉
本文原本是針對(duì)守護(hù)線程的一些探討,感覺知識(shí)點(diǎn)稍顯淺薄,故基于原有文章進(jìn)行迭代補(bǔ)充對(duì)于Java程序優(yōu)雅關(guān)閉的一些思考。
一、JVM中的關(guān)閉
1. 詳解虛擬機(jī)鉤子
在Java進(jìn)程開發(fā)中,對(duì)于重量級(jí)的系統(tǒng)資源關(guān)閉或者進(jìn)程資源整理或信號(hào)輸出,常常會(huì)通過Java內(nèi)置的addShutdownHook方法注冊(cè)回調(diào)函數(shù),確保在Java進(jìn)程關(guān)閉不再使用這些資源時(shí)將其釋放,例如hutool這個(gè)工具類對(duì)應(yīng)連接池的管理工具GlobalDSFactory,其底層就會(huì)在類加載初始化時(shí)利用addShutdownHook注冊(cè)一個(gè)連接池銷毀的回調(diào)函數(shù):
/*
* 設(shè)置在JVM關(guān)閉時(shí)關(guān)閉所有數(shù)據(jù)庫(kù)連接
*/
static {
// JVM關(guān)閉時(shí)關(guān)閉所有連接池
Runtime.getRuntime().addShutdownHook(new Thread() {
@Override
public void run() {
if (null != factory) {
factory.destroy();
StaticLog.debug("DataSource: [{}] destroyed.", factory.dataSourceName);
factory = null;
}
}
});
}
而虛擬機(jī)鉤子注冊(cè)的原理本質(zhì)上就是在調(diào)用addShutdownHook時(shí),其底層將這個(gè)現(xiàn)場(chǎng)hook注冊(cè)到一個(gè)hooks的map容器中,并在shutdown的時(shí)候遍歷調(diào)用這些hook線程:
對(duì)應(yīng)的我們也給出addShutdownHook的實(shí)現(xiàn),可以看到其底層就是調(diào)用ApplicationShutdownHooks來注冊(cè)hook:
public void addShutdownHook(Thread hook) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new RuntimePermission("shutdownHooks"));
}
ApplicationShutdownHooks.add(hook);
}
而步入這個(gè)add方法后可以看到其內(nèi)部本質(zhì)上就是在必要的校驗(yàn)后,存入到hooks這個(gè)map中:
private static IdentityHashMap<Thread, Thread> hooks;
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);
}
當(dāng)觸發(fā)虛擬機(jī)鉤子關(guān)閉時(shí),其內(nèi)部就會(huì)針對(duì)hooks進(jìn)行遍歷并按照如下邏輯處理:
- 將hook線程啟動(dòng),執(zhí)行hook邏輯
- 調(diào)用join確保該hook能夠準(zhǔn)確執(zhí)行完成
static void runHooks() {
Collection<Thread> threads;
synchronized(ApplicationShutdownHooks.class) {
threads = hooks.keySet();
hooks = null;
}
//遍歷hook線程啟動(dòng)
for (Thread hook : threads) {
hook.start();
}
for (Thread hook : threads) {
while (true) {
try {
//調(diào)用join加入主線程確保當(dāng)前線程能夠正確執(zhí)行完成
hook.join();
break;
} catch (InterruptedException ignored) {
}
}
}
}
當(dāng)所有關(guān)閉鉤子都執(zhí)行結(jié)束時(shí),如果runFinalizersOnExit為true,那么JVM就會(huì)運(yùn)行終結(jié)器finalizers,此時(shí)JVM并不會(huì)停止或者關(guān)閉仍然在運(yùn)行的應(yīng)用線程。直到最終JVM結(jié)束,應(yīng)用線程才會(huì)被關(guān)閉,對(duì)應(yīng)的我們可以在源碼Shutdown的exit方法印證:
static void exit(int status) {
boolean runMoreFinalizers = false;
synchronized (lock) {
//......
case FINALIZERS:
if (status != 0) {
/* Halt immediately on nonzero status */
halt(status);
} else {
//......
//將runFinalizersOnExit賦值給runMoreFinalizers
runMoreFinalizers = runFinalizersOnExit;
}
break;
}
}
//如果runMoreFinalizers 為true,則運(yùn)行終結(jié)器
if (runMoreFinalizers) {
runAllFinalizers();
halt(status);
}
//......
}
2. 虛擬機(jī)鉤子串行化使用
需要注意的虛擬機(jī)鉤子注冊(cè)后的調(diào)用時(shí)機(jī),當(dāng)JVM執(zhí)行關(guān)閉鉤子的時(shí)候,如果守護(hù)或者非守護(hù)線程也在運(yùn)行,那么虛擬機(jī)鉤子就可能和這些線程并發(fā)的執(zhí)行,即虛擬機(jī)鉤子可能會(huì)并行的執(zhí)行一些工作,所以對(duì)于一些存在依賴性的共享數(shù)據(jù)操作,虛擬機(jī)鉤子要慎重使用。
例如我們用虛擬機(jī)鉤子將日志服務(wù)關(guān)閉,此時(shí)如果另外的虛擬機(jī)鉤子需要使用日志打印,可能就會(huì)報(bào)錯(cuò):
例如我們的日志框架LogService ,本質(zhì)上就是對(duì)于文件流的寫入和關(guān)閉:
static class LogService {
private static final BufferedWriter writer = FileUtil.getWriter("F:\\tmp\\log.txt", Charset.defaultCharset(), true);
@SneakyThrows
public void log(String msg) {//將數(shù)據(jù)寫入日志中
writer.write(msg);
}
public void close() {
try {
writer.close();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
如下圖所說,若在虛擬機(jī)鉤子上注冊(cè)關(guān)閉打印和關(guān)閉日志框架的鉤子,就有可能出現(xiàn)打印鉤子拋出stream close的錯(cuò)誤:
LogService logService = new LogService();
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
//拋出stream close的錯(cuò)誤
logService.log("hello world");
}));
/**
* 注冊(cè)虛擬機(jī)鉤子
*/
Runtime.getRuntime().addShutdownHook(new Thread(() -> {
//執(zhí)行一些應(yīng)用程序的資源關(guān)閉
logService.close();
}));
總的來說,使用虛擬機(jī)鉤子必須注意:
- 虛擬機(jī)鉤子要保證線程安全,即針對(duì)共享資源做好同步把控
- 虛擬機(jī)鉤子盡量串行化執(zhí)行,且鉤子之間不可以有任何依賴
- 關(guān)閉鉤子應(yīng)該盡快的退出,因?yàn)樗苯拥臎Q定的JVM退出的結(jié)束時(shí)間
二、守護(hù)線程
1. 守護(hù)線程的基本概念
很多人對(duì)守護(hù)線程都不陌生,對(duì)于守護(hù)線程大部分讀者都停留在JDK官方文檔所介紹的概念:
The Java Virtual Machine exits when the only threads running are all daemon threads.
文檔的意思是當(dāng)JVM中不存在任何一個(gè)正在運(yùn)行的非守護(hù)線程時(shí),JVM進(jìn)程會(huì)直接退出。
讀起來很拗口對(duì)不對(duì),沒關(guān)系,本文就會(huì)基于幾個(gè)代碼示例,讓你更深層次的理解守護(hù)線程。在此之前,讀者不妨自測(cè)一下,下面這幾道面試題:
- 守護(hù)線程和普通線程有什么區(qū)別?
- 守護(hù)線程默認(rèn)優(yōu)先級(jí)是多少?
- 若父線程為守護(hù)線程,在其內(nèi)部創(chuàng)建一個(gè)普通線程,父線程停止,子線程是否也會(huì)停止呢?
- 如何創(chuàng)建守護(hù)線程池?
- 守護(hù)線程使用有哪些注意事項(xiàng)?
2. 守護(hù)線程和普通線程的區(qū)別
要了解區(qū)別就先來了解一下兩者的使用,非守護(hù)線程,也就我們?nèi)粘?chuàng)建的普通線程,可以看到這段代碼創(chuàng)建了一個(gè)普通線程,在無限循環(huán)的定時(shí)輸出內(nèi)容,而主線程僅僅是輸出一段文字后就不做任何動(dòng)作了。
public static void main(String[] args) {
Thread t = new Thread(() -> {
while (true) {
log.info("普通線程執(zhí)行了......");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
t.start();
log.info("主線程運(yùn)行結(jié)束");
}
對(duì)應(yīng)的輸出結(jié)果如下,可以看到,即使主線程停止運(yùn)行了,而非守護(hù)線程也仍然會(huì)在運(yùn)行,也就是JDK官方文檔的字面含義,普通線程不停止,JVM就不停止運(yùn)行:
12:44:57.022 [Thread-0] INFO com.sharkChili.webTemplate.Main - 普通線程執(zhí)行了......
12:44:57.022 [main] INFO com.sharkChili.webTemplate.Main - 主線程運(yùn)行結(jié)束
12:45:02.031 [Thread-0] INFO com.sharkChili.webTemplate.Main - 普通線程執(zhí)行了......
基于上述代碼,用setDaemon(true)將該線程設(shè)置為守護(hù)線程:
public static void main(String[] args) {
Thread t = new Thread(() -> {
while (true) {
log.info("守護(hù)線程執(zhí)行了......");
try {
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
//設(shè)置當(dāng)前線程為守護(hù)線程
t.setDaemon(true);
t.start();
log.info("主線程運(yùn)行結(jié)束");
}
輸出結(jié)果如下,可以看到隨著主線程的消亡,守護(hù)線程也會(huì)隨之停止,不再運(yùn)行,自此我相信讀者可以理解JDK官方文檔所說的那句話了,只要有一個(gè)普通線程在,JVM就不會(huì)退出,只要所有普通線程停止工作,JVM自動(dòng)退出,守護(hù)線程也會(huì)自動(dòng)結(jié)束。
12:44:23.239 [Thread-0] INFO com.sharkChili.webTemplate.Main - 守護(hù)線程執(zhí)行了......
12:44:23.239 [main] INFO com.sharkChili.webTemplate.Main - 主線程運(yùn)行結(jié)束
3. 守護(hù)線程和普通線程優(yōu)先級(jí)的區(qū)別
我們可以通過getPriority方法查看兩者的區(qū)別:
public static void main(String[] args) {
Thread t = new Thread(() -> {
log.info("守護(hù)線程優(yōu)先級(jí):{}", Thread.currentThread().getPriority());
});
//設(shè)置當(dāng)前線程為守護(hù)線程
t.setDaemon(true);
t.start();
log.info("主線程運(yùn)行結(jié)束,當(dāng)前線程運(yùn)行優(yōu)先級(jí):{}", Thread.currentThread().getPriority());
}
從輸出結(jié)果來看,兩者的優(yōu)先級(jí)是一樣的,都為5:
12:54:36.344 [main] INFO com.sharkChili.webTemplate.Main - 主線程運(yùn)行結(jié)束,當(dāng)前線程運(yùn)行優(yōu)先級(jí):5
12:54:36.344 [Thread-0] INFO com.sharkChili.webTemplate.Main - 守護(hù)線程優(yōu)先級(jí):5
4. 父守護(hù)線程問題
我們創(chuàng)建了一個(gè)守護(hù)線程,在其runnable實(shí)現(xiàn)中創(chuàng)建一個(gè)子線程:
public static void main(String[] args) {
Thread parentThread = new Thread(() -> {
Thread childThread = new Thread(() -> {
while (true) {
log.info("子線程運(yùn)行中,是否為守護(hù)線程:{}",Thread.currentThread().isDaemon());
try {
TimeUnit.HOURS.sleep(1);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
childThread.start();
log.info("parentThread守護(hù)線程運(yùn)行中");
});
//設(shè)置當(dāng)前線程為守護(hù)線程
parentThread.setDaemon(true);
parentThread.start();
log.info("主線程運(yùn)行結(jié)束");
}
從輸出結(jié)果來看,父線程為守護(hù)線程時(shí),其內(nèi)部創(chuàng)建的子線程也為守護(hù)線程,所以隨著父線程的銷毀,子線程也會(huì)同步銷毀。
00:05:56.869 [Thread-1] INFO com.sharkChili.webTemplate.Main - 子線程運(yùn)行中,是否為守護(hù)線程:true
00:05:56.869 [main] INFO com.sharkChili.webTemplate.Main - 主線程運(yùn)行結(jié)束
00:05:56.869 [Thread-0] INFO com.sharkChili.webTemplate.Main - parentThread守護(hù)線程運(yùn)行中
5. 守護(hù)線程池的創(chuàng)建
public static void main(String[] args) {
ExecutorService threadPool = Executors.newFixedThreadPool(10, ThreadFactoryBuilder.create()
.setNamePrefix("worker-")
.setDaemon(true)
.build());
threadPool.execute(()->{
while (true){
try {
log.info("守護(hù)線程運(yùn)行了");
Thread.sleep(5000);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
log.info("主線程退出");
}
6. 守護(hù)線程的使用場(chǎng)景
因?yàn)槭刈o(hù)線程擁有自動(dòng)結(jié)束自己生命周期的特性,當(dāng)JVM中沒有一個(gè)普通線程運(yùn)行時(shí),JVM會(huì)退出,即所有守護(hù)線程會(huì)自動(dòng)停止,所以守護(hù)線程的使用場(chǎng)景可以有以下幾種:
- 垃圾回收線程就是典型的守護(hù)線程,在后臺(tái)進(jìn)行垃圾對(duì)象回收的工作。
- 非核心業(yè)務(wù)工作可交由守護(hù)線程,例如:各類信息統(tǒng)計(jì)、服務(wù)監(jiān)控等,一旦進(jìn)程結(jié)束運(yùn)行則這些守護(hù)線程停止工作。
7. 守護(hù)線程注意事項(xiàng)
- 復(fù)雜計(jì)算、資源回收這種不建議使用守護(hù)線程。
- setDaemon要在start方法前面,否者該設(shè)置會(huì)不生效。
三、finalize關(guān)閉的哲學(xué)
1. 基本介紹
針對(duì)一些系統(tǒng)資源例如文件句柄或者套接字句柄,當(dāng)不需要它們時(shí),垃圾回收器定義了finalize方法進(jìn)行一些資源關(guān)閉,一旦垃圾回收器回收這些對(duì)象之后,對(duì)應(yīng)的資源就會(huì)調(diào)用finalize釋放。
例如FileInputStream的finalize方法,它就會(huì)檢查當(dāng)前文件句柄是否非空,然后顯示的調(diào)用一下close方法:
protected void finalize() throws IOException {
if ((fd != null) && (fd != FileDescriptor.in)) {
//關(guān)閉文件句柄
close();
}
}
2. 終結(jié)器注意事項(xiàng)和正確資源關(guān)閉姿勢(shì)
需要注意的finalize在JVM運(yùn)行中可能會(huì)執(zhí)行也可能不會(huì)執(zhí)行,JVM對(duì)此無法做出保證,所以它運(yùn)行時(shí)存著極端的不確定性,所以進(jìn)行資源關(guān)閉時(shí),我們非常不建議使用finalize。
正確的一些系統(tǒng)資源關(guān)閉回收,筆者更建議是使用階段采用try-with-resource手動(dòng)關(guān)閉資源:
//使用try-with-resource手動(dòng)關(guān)閉資源
try(BufferedReader reader = FileUtil.getUtf8Reader("filePahth")){
System.out.println(reader.readLine());
}catch (Exception e){
//異常處理
}