1. 程式人生 > >從 JVM 視角看看 Java 守護執行緒

從 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執行緒優先順序,更要知道對應作業系統的優先順序,不然會踩坑

執行緒最最基礎的知識

老闆叫你別阻塞了

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

泡一杯茶,學一學同異步

程序知多少?

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

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

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

本文由部落格一文多發平臺 OpenWrite 釋出!