從 JVM 視角看看 Java 守護線程

Java 多線程系列第 7 篇。

這篇我們來講講線程的另一個特性:守護線程 or 用戶線程?

我們先來看看 Thread.setDaemon() 方法的註釋,如下所示。

  1. Marks this thread as either a daemon thread or a user thread.
  1. The Java Virtual Machine exits when the only threads running are all daemon threads.
  2. This method must be invoked before the thread is started.

裏面提到了 3 點信息,一一來做下解釋:

官方特性

1. 用戶線程 or 守護線程?

把 Java 線程分成 2 類,一類是用戶線程,也就是我們創建線程時,默認的一類線程,屬性 daemon = false;另一類是守護線程,當我們設置 daemon = true 時,就是這類線程。

兩者的一般關係是:用戶線程就是運行在前臺的線程,守護線程就是運行在後臺的線程,一般情況下,守護線程是爲用戶線程提供一些服務。比如在 Java 中,我們常說的 GC 內存回收線程就是守護線程。

2. JVM 與用戶線程共存亡

上面第二點翻譯過來是:當所有用戶線程都執行完,只存在守護線程在運行時,JVM 就退出。看了網上資料以及一些書籍,全都有這句話,但是也都只是有這句話,沒有講明是爲啥,好像這句話就成了定理,不需要證明的樣子。既然咱最近搭建了 JVM Debug 環境,那就得來查個究竟。(查得好辛苦,花了很久的時間才查出來)

我們看到 JVM 源碼 thread.cpp 文件,這裏是實現線程的代碼。我們通過上面那句話,說明是有一個地方監測着當前非守護線程的數量,不然怎麼知道現在只剩下守護線程呢?很有可能是在移除線程的方法裏面,跟着這個思路,我們看看該文件的 remove() 方法。代碼如下。

/**
 * 移除線程 p
 */
void Threads::remove(JavaThread* p, bool is_daemon) {

  // Reclaim the ObjectMonitors from the omInUseList and omFreeList of the moribund thread.
  ObjectSynchronizer::omFlush(p);

  /**
   * 創建一個監控鎖對象 ml
   */
  // Extra scope needed for Thread_lock, so we can check
  // that we do not remove thread without safepoint code notice
  { MonitorLocker ml(Threads_lock);

    assert(ThreadsSMRSupport::get_java_thread_list()->includes(p), "p must be present");

    // Maintain fast thread list
    ThreadsSMRSupport::remove_thread(p);

    // 當前線程數減 1
    _number_of_threads--;
    if (!is_daemon) {
        /**
         * 非守護線程數量減 1
         */
      _number_of_non_daemon_threads--;

      /**
       * 當非守護線程數量爲 1 時,喚醒在 destroy_vm() 方法等待的線程
       */
      // Only one thread left, do a notify on the Threads_lock so a thread waiting
      // on destroy_vm will wake up.
      if (number_of_non_daemon_threads() == 1) {
        ml.notify_all();
      }
    }
    /**
     * 移除掉線程
     */
    ThreadService::remove_thread(p, is_daemon);

    // Make sure that safepoint code disregard this thread. This is needed since
    // the thread might mess around with locks after this point. This can cause it
    // to do callbacks into the safepoint code. However, the safepoint code is not aware
    // of this thread since it is removed from the queue.
    p->set_terminated_value();
  } // unlock Threads_lock

  // Since Events::log uses a lock, we grab it outside the Threads_lock
  Events::log(p, "Thread exited: " INTPTR_FORMAT, p2i(p));
}

我在裏面加了一些註釋,可以發現,果然是我們想的那樣,裏面有記錄着非守護線程的數量,而且當非守護線程爲 1 時,就會喚醒在 destory_vm() 方法裏面等待的線程,我們確認已經找到 JVM 在非守護線程數爲 1 時會觸發喚醒監控 JVM 退出的線程代碼。緊接着我們看看 destory_vm() 代碼,同樣是在 thread.cpp 文件下。

bool Threads::destroy_vm() {
  JavaThread* thread = JavaThread::current();

#ifdef ASSERT
  _vm_complete = false;
#endif
  /**
   * 等待自己是最後一個非守護線程條件
   */
  // Wait until we are the last non-daemon thread to execute
  { MonitorLocker nu(Threads_lock);
    while (Threads::number_of_non_daemon_threads() > 1)
        /**
         * 非守護線程數大於 1,則一直等待
         */
      // This wait should make safepoint checks, wait without a timeout,
      // and wait as a suspend-equivalent condition.
      nu.wait(0, Mutex::_as_suspend_equivalent_flag);
  }

  /**
   * 下面代碼是關閉 VM 的邏輯
   */
  EventShutdown e;
  if (e.should_commit()) {
    e.set_reason("No remaining non-daemon Java threads");
    e.commit();
  }
  ...... 省略餘下代碼
}

我們這裏看到當非守護線程數量大於 1 時,就一直等待,直到剩下一個非守護線程時,就會在線程執行完後,退出 JVM。這時候又有一個點需要定位,什麼時候調用 destroy_vm() 方法呢?還是通過查看代碼以及註釋,發現是在 main() 方法執行完成後觸發的。

java.c 文件的 JavaMain() 方法裏面,最後執行完調用了 LEAVE() 方法,該方法調用了 (*vm)->DestroyJavaVM(vm); 來觸發 JVM 退出,最終調用 destroy_vm() 方法。

#define LEAVE() \
    do { \
        if ((*vm)->DetachCurrentThread(vm) != JNI_OK) { \
            JLI_ReportErrorMessage(JVM_ERROR2); \
            ret = 1; \
        } \
        if (JNI_TRUE) { \
            (*vm)->DestroyJavaVM(vm); \
            return ret; \
        } \
    } while (JNI_FALSE)

所以我們也知道了,爲啥 main 線程可以比子線程先退出?雖然 main 線程退出前調用了 destroy_vm() 方法,但是在 destroy_vm() 方法裏面等待着非守護線程執行完,子線程如果是非守護線程,則 JVM 會一直等待,不會立即退出。

我們對這個點總結一下:Java 程序在 main 線程執行退出時,會觸發執行 JVM 退出操作,但是 JVM 退出方法 destroy_vm() 會等待所有非守護線程都執行完,裏面是用變量 number_of_non_daemon_threads 統計非守護線程的數量,這個變量在新增線程和刪除線程時會做增減操作

另外衍生一點就是:當 JVM 退出時,所有還存在的守護線程會被拋棄,既不會執行 finally 部分代碼,也不會執行 stack unwound 操作(也就是也不會 catch 異常)。這個很明顯,JVM 都退出了,守護線程自然退出了,當然這是守護線程的一個特性。

3. 是男是女?生下來就註定了

這個比較好理解,就是線程是用戶線程還是守護線程,在線程還未啓動時就得確定。在調用 start() 方法之前,還只是個對象,沒有映射到 JVM 中的線程,這個時候可以修改 daemon 屬性,調用 start() 方法之後,JVM 中就有一個線程映射這個線程對象,所以不能做修改了。

其他的特性

1.守護線程屬性繼承自父線程

這個咱就不用寫代碼來驗證了,直接看 Thread 源代碼構造方法裏面就可以知道,代碼如下所示。

private Thread(ThreadGroup g, Runnable target, String name,
               long stackSize, AccessControlContext acc,
               boolean inheritThreadLocals) {
   ...省略一堆代碼
    this.daemon = parent.isDaemon();
   ...省略一堆代碼
}

2.守護線程優先級比用戶線程低

看到很多書籍和資料都這麼說,我也很懷疑。所以寫了下面代碼來測試是不是守護線程優先級比用戶線程低?

public class TestDaemon {
    static AtomicLong daemonTimes = new AtomicLong(0);
    static AtomicLong userTimes = new AtomicLong(0);

    public static void main(String[] args) {
        int count = 2000;
        List<MyThread> threads = new ArrayList<>(count);
        for (int i = 0; i < count; i ++) {
            MyThread userThread = new MyThread();
            userThread.setDaemon(false);
            threads.add(userThread);

            MyThread daemonThread = new MyThread();
            daemonThread.setDaemon(true);
            threads.add(daemonThread);
        }

        for (int i = 0; i < count; i++) {
            threads.get(i).start();
        }

        try {
            Thread.sleep(10000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        System.out.println("daemon 統計:" + daemonTimes.get());
        System.out.println("user 統計:" + userTimes.get());
        System.out.println("daemon 和 user 相差時間:" + (daemonTimes.get() - userTimes.get()) + "ms");

    }

    static class MyThread extends Thread {
        @Override
        public void run() {
            if (this.isDaemon()) {
                daemonTimes.getAndAdd(System.currentTimeMillis());
            } else {
                userTimes.getAndAdd(System.currentTimeMillis());
            }
        }
    }
}

運行結果如下。

結果1:
daemon 統計:1570785465411405
user 統計:1570785465411570
daemon 和 user 相差時間:-165ms

結果2:
daemon 統計:1570786615081403
user 統計:1570786615081398
daemon 和 user 相差時間:5ms

是不是很驚訝,居然相差無幾,但是這個案例我也不能下定義說:守護線程和用戶線程優先級是一樣的。看了 JVM 代碼也沒找到守護線程優先級比用戶線程低,這個點還是保持懷疑,有了解的朋友可以留言說一些,互相交流學習。

總結

總結一下這篇文章講解的點,一個是線程被分爲 2 種類型,一種是用戶線程,另一種是守護線程;如果要把線程設置爲守護線程,需要在線程調用start()方法前設置 daemon 屬性;還有從 JVM 源碼角度分析爲什麼當用戶線程都執行完的時候,JVM 會自動退出。接着講解了守護線程有繼承性,父線程是守護線程,那麼子線程默認就是守護線程;另外對一些書籍和資料所說的 守護線程優先級比用戶線程低 提出自己的疑問,並希望有了解的朋友能幫忙解答。

如果覺得這篇文章看了有收穫,麻煩點個在看,支持一下,原創不易。

推薦閱讀

寫了那麼多年 Java 代碼,終於 debug 到 JVM 了

原創 | 全網最新最簡單的 openjdk13 代碼編譯

瞭解Java線程優先級,更要知道對應操作系統的優先級,不然會踩坑

線程最最基礎的知識

老闆叫你別阻塞了

吃個快餐都能學到串行、並行、併發

泡一杯茶,學一學同異步

進程知多少?

設計模式看了又忘,忘了又看?

後臺回覆『設計模式』可以獲取《一故事一設計模式》電子書

覺得文章有用幫忙轉發&點贊,多謝朋友們!

LieBrother

本文由博客一文多發平臺 OpenWrite 發佈!
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章