守护线程和钩子

本文深入探讨了Java守护线程的概念,指出守护线程会在所有用户线程结束后终止,导致虚拟机退出。分析了JVM如何管理用户线程和守护线程,包括可能的线程关闭策略,并通过代码示例和JVM源码片段解释了线程退出的过程,特别是DestroyJavaVM线程的角色和创建时机。
摘要由CSDN通过智能技术生成

什么是守护线程?

守护线程就是在所有用户线程结束之后,所有守护线程的终止,进而虚拟机进程就会退出。
呃, 这里为什么把用户线程加粗呢? 想一个问题,如果用户线程代表 除了守护线程之后的所有线程包括(我们自己创建的以及jvm里面c++代码创建的),守护线程会自己终止吗?

先不说代码(毕竟jvm 层的c++代码可太难了),就说一般的idea如果让你来设计 如何实现?

方式1: 所有守护线程都会循环有个全局的标志位判断,比如一个静态变量,在最后一个用户线程退出的时候 把找个标志位复制为true, 这个时候守护线程就执行return。

这种方式可取吗? 那也太麻烦了, 用户要么写守护线程的时候自己来判断这个标记位,或者使用动态代理的方式为runnable 织入横切逻辑 ,这也坑定不可取的

方式2: 线程是由操作系统提供的创建函数的(我想称为函数对于c 面向过程的语言, 像c++的类的函数,我想称为行为或者方法)。那么操作系统也应该会提供停止正在运行的线程的函数,比如我们只要把第一次调操作系统创建线程得到的tid线程id,传给操作系统的停止线程的函数就可以。

那要实现方式二的关键点在哪里: 要执行的关闭线程集合从哪里来? 谁来关闭这些线程集合? 基于这两个点可以考虑把 守护线程和用户线程的线程集合存在一个公共的存储能力比如 数组、链表等, 这里推荐链表, 好的, 那接着 谁来执行这个关系的行为呢? ok 那就可以来一个 专门的用户线程来最后执行 对守护线程的线程集合的关闭操作

那么接着 问题又来了? 这个专门清楚 守护线程的用户线程我们称之为DestroyJavaVM 吧, 那这个线程的创建时机是什么时候?

我们知道 java程序 也就是一个class文件 一般如果要可执行都得又一个 mian方法。这个main方法是jvm在 底层内部启动一个线程 通过jni的方式来回调 java的main方法, 所以一般jvm中一个java程序运行都会有一个线程来执行main方法,所以这个main方法执行结束之后 可以创建一个DestroyJavaVM 线程来执行 销毁守护线程的逻辑。但是问题又来了,
如果这时候还有别的我们自己定义的用户线程呢?所以DestroyJavaVM 在执行销毁逻辑之前得对 用户线程集合进行判断,如果大于1(他自己和别人)就进行wait, 如果另一个用户线程退出的时候判断用户线程数为1那么就notify把DestroyJavaVM唤醒执行销毁逻辑进行。

动手验证一下:

通过idea的debug查询DestroyJavaVM线程

在这里插入图片描述

🆗 在想一下, 这个时候 执行main方法的mian线程还没有结束,这个时候创建DestroyJavaVM 是没必要的,浪费资源,所以jvm就把DestroyJavaVM线程的创建留在了 main线程退出之后, 那么怎么验证呢? 我们需要new 一个thread.
在这里插入图片描述
🆗 现在不就出现了。 哈哈哈。 但是注意没有 是 RUNNING状态, 按道理来说应该 是wait呀,🆗,继续验证,我们别使用debug , 使用sleep和打印看看。

public class DaemonThreads {
    
    /**
     * 
     * @author: puhg_sinosoft
     * @date: 2022/6/25 1:17
     * @param  
     */
    public static void printDestoryVMState(){
        try {
            TimeUnit.SECONDS.sleep(3);
            Thread thread = Thread.currentThread();
            ThreadGroup threadGroup = thread.getThreadGroup();
            int i = threadGroup.activeCount();
            Thread[] threads = new Thread[i];
            threadGroup.enumerate(threads);
            Arrays.stream(threads).filter(x-> x.getName().equals("DestroyJavaVM")).forEach(x-> System.out.println(x.getState()));
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
    
    public static void main(String[] args) {
        new Thread(()->{
            try {
                TimeUnit.SECONDS.sleep(3);
                printDestoryVMState();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        new Thread(()->{
            try {
                TimeUnit.SECONDS.sleep(6);
                printDestoryVMState();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }).start();
        System.out.println("mian线程结束");
    }
}

呃。。。。说实话还是 打印出 RUNABLE, 不应该是 WAIT吗。

好吧 看 大概 看一个,盲猜一下 。c++ 代码。

在这里插入图片描述
在这里插入图片描述
呃 jvm代码注释 白嫖大佬,得, 别管我看得看不懂就是 猜, 这里是 jvm c程序的 main方法入口。
呃如果要看如果调用起来的请看 java.cass 文件的main方法如何调用起来
最后回调完java的mian方法之后,线程结束mian方法退出,就要启动DestroyJavaVM线程了。

int JNICALL
JavaMain(void * _args) //这个方法使用操作系统的线程, 用来回调java的main线程
{
    JavaMainArgs *args = (JavaMainArgs *)_args;
    int argc = args->argc;
    char **argv = args->argv;
    int mode = args->mode;
    char *what = args->what;
    InvocationFunctions ifn = args->ifn;

    JavaVM *vm = 0;
    JNIEnv *env = 0;
    jclass mainClass = NULL;
    jclass appClass = NULL; // actual application class being launched
    jmethodID mainID;
    jobjectArray mainArgs;
    int ret = 0;
    jlong start, end;

    RegisterThread();

    /* Initialize the virtual machine */
    start = CounterGet();
    if (!InitializeJVM(&vm, &env, &ifn)) {   //初始化虚拟机,内部创建虚拟机并开启main线程
        JLI_ReportErrorMessage(JVM_ERROR1);
        exit(1);
    }

    if (showSettings != NULL) {
        ShowSettings(env, showSettings);
        CHECK_EXCEPTION_LEAVE(1);
    }

    if (printVersion || showVersion) {
        PrintJavaVersion(env, showVersion);
        CHECK_EXCEPTION_LEAVE(0);
        if (printVersion) {
            LEAVE();
        }
    }

    /* If the user specified neither a class name nor a JAR file */
    if (printXUsage || printUsage || what == 0 || mode == LM_UNKNOWN) {
        PrintUsage(env, printXUsage);
        CHECK_EXCEPTION_LEAVE(1);
        LEAVE();
    }

    FreeKnownVMs();  /* after last possible PrintUsage() */

    if (JLI_IsTraceLauncher()) {
        end = CounterGet();
        JLI_TraceLauncher("%ld micro seconds to InitializeJVM\n",
               (long)(jint)Counter2Micros(end-start));
    }

    /* At this stage, argc/argv have the application's arguments */
    if (JLI_IsTraceLauncher()){
        int i;
        printf("%s is '%s'\n", launchModeNames[mode], what);
        printf("App's argc is %d\n", argc);
        for (i=0; i < argc; i++) {
            printf("    argv[%2d] = '%s'\n", i, argv[i]);
        }
    }

    ret = 1;

    /*
     * Get the application's main class.
     *
     * See bugid 5030265.  The Main-Class name has already been parsed
     * from the manifest, but not parsed properly for UTF-8 support.
     * Hence the code here ignores the value previously extracted and
     * uses the pre-existing code to reextract the value.  This is
     * possibly an end of release cycle expedient.  However, it has
     * also been discovered that passing some character sets through
     * the environment has "strange" behavior on some variants of
     * Windows.  Hence, maybe the manifest parsing code local to the
     * launcher should never be enhanced.
     *
     * Hence, future work should either:
     *     1)   Correct the local parsing code and verify that the
     *          Main-Class attribute gets properly passed through
     *          all environments,
     *     2)   Remove the vestages of maintaining main_class through
     *          the environment (and remove these comments).
     *
     * This method also correctly handles launching existing JavaFX
     * applications that may or may not have a Main-Class manifest entry.
     */
    mainClass = LoadMainClass(env, mode, what);   //加载main函数所在的类文件
    CHECK_EXCEPTION_NULL_LEAVE(mainClass);
    /*
     * In some cases when launching an application that needs a helper, e.g., a
     * JavaFX application with no main method, the mainClass will not be the
     * applications own main class but rather a helper class. To keep things
     * consistent in the UI we need to track and report the application main class.
     */
    appClass = GetApplicationClass(env);//JavaFX没有MainClass而是通过ApplicationClass启动的,这里获取ApplicationClass
    NULL_CHECK_RETURN_VALUE(appClass, -1);
    /*
     * PostJVMInit uses the class name as the application name for GUI purposes,
     * for example, on OSX this sets the application name in the menu bar for
     * both SWT and JavaFX. So we'll pass the actual application class here
     * instead of mainClass as that may be a launcher or helper class instead
     * of the application class.
     */
    PostJVMInit(env, appClass, vm);//将ApplicationClass作为应用名传给JavaFX本身,比如作为主菜单
    /*
     * The LoadMainClass not only loads the main class, it will also ensure
     * that the main method's signature is correct, therefore further checking
     * is not required. The main method is invoked here so that extraneous java
     * stacks are not in the application stack trace.
     */
    mainID = (*env)->GetStaticMethodID(env, mainClass, "main",
                                       "([Ljava/lang/String;)V");//获取main方法的方法ID
    CHECK_EXCEPTION_NULL_LEAVE(mainID);

    /* Build platform specific argument array */
    mainArgs = CreateApplicationArgs(env, argv, argc);  //解析main方法的参数
    CHECK_EXCEPTION_NULL_LEAVE(mainArgs);

    /* Invoke main method. */
    (*env)->CallStaticVoidMe thod(env, mainClass, mainID, mainArgs);  //执行main方法

    /*
     * The launcher's exit code (in the absence of calls to
     * System.exit) will be non-zero if main threw an exception.
     */
    ret = (*env)->ExceptionOccurred(env) == NULL ? 0 : 1;
    LEAVE();//main方法执行完毕,JVM退出,包含两步:(*vm)->DetachCurrentThread,让当前Main线程同启动线程断联,然后创建一个新的名为DestroyJavaVM的线程,让该线程等待所有的非后台进程退出,并在最后执行(*vm)->DestroyJavaVM方法。
}
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  等待自己是最后一个非守护线程条件
  { MutexLocker nu(Threads_lock);
    while (Threads::number_of_non_daemon_threads() > 1 )  //非守护线程大于1,则一直等待(等到等于1的时候往下执行)
      // This wait should make safepoint checks, wait without a timeout,
      // and wait as a suspend-equivalent condition.
      //
      // Note: If the FlatProfiler is running and this thread is waiting
      // for another non-daemon thread to finish, then the FlatProfiler
      // is waiting for the external suspend request on this thread to
      // complete. wait_for_ext_suspend_completion() will eventually
      // timeout, but that takes time. Making this wait a suspend-
      // equivalent condition solves that timeout problem.
      //
      Threads_lock->wait(!Mutex::_no_safepoint_check_flag, 0,
                         Mutex::_as_suspend_equivalent_flag);  //这里阻塞的线程会被remove方法里的notify唤醒
  }

  // Hang forever on exit if we are reporting an error.
  if (ShowMessageBoxOnError && is_error_reported()) {
    os::infinite_sleep();
  }
  os::wait_for_keypress_at_exit();

  if (JDK_Version::is_jdk12x_version()) {
    // We are the last thread running, so check if finalizers should be run.
    // For 1.3 or later this is done in thread->invoke_shutdown_hooks()
    HandleMark rm(thread);
    Universe::run_finalizers_on_exit();
  } else {
    // run Java level shutdown hooks
    thread->invoke_shutdown_hooks();//钩子代码是在这里触发的(设置时间长点,守护线程可以再存活一会儿)
  }

  before_exit(thread);

  thread->exit(true);//destroyJavaVM线程退出,至此java中不能执行任何代码

  // Stop VM thread.
  {
    // 4945125 The vm thread comes to a safepoint during exit.
    // GC vm_operations can get caught at the safepoint, and the
    // heap is unparseable if they are caught. Grab the Heap_lock
    // to prevent this. The GC vm_operations will not be able to
    // queue until after the vm thread is dead.
    // After this point, we'll never emerge out of the safepoint before
    // the VM exits, so concurrent GC threads do not need to be explicitly
    // stopped; they remain inactive until the process exits.
    // Note: some concurrent G1 threads may be running during a safepoint,
    // but these will not be accessing the heap, just some G1-specific side
    // data structures that are not accessed by any other threads but them
    // after this point in a terminal safepoint.

    MutexLocker ml(Heap_lock);

    VMThread::wait_for_vm_thread_exit();
    assert(SafepointSynchronize::is_at_safepoint(), "VM thread should exit at Safepoint");
    VMThread::destroy();
  }

线程退出的执行:

void Threads::remove(JavaThread* p) {//移除线程
  // Extra scope needed for Thread_lock, so we can check
  // that we do not remove thread without safepoint code notice
  { MutexLocker ml(Threads_lock);

    assert(includes(p), "p must be present");

    JavaThread* current = _thread_list;
    JavaThread* prev    = NULL;

    while (current != p) { //线程以链表形式存在,判断当前线程是否要删除的线程,如果不是,后移,直到找到为止
      prev    = current;
      current = current->next();
    }

    if (prev) {//被删除线程是否有前驱节点,如果有,则直接摘除,让前驱节点指向被删除节点的next节点(跨过被删除节点)
      prev->set_next(current->next());
    } else { //如果没有前驱节点,说明被删除节点就是第一个节点,直接_thread_list指向它的next节点
      _thread_list = p->next();
    }
    _number_of_threads--; //线程数量减一
    oop threadObj = p->threadObj();
    bool daemon = true;
    if (threadObj == NULL || !java_lang_Thread::is_daemon(threadObj)) {
      _number_of_non_daemon_threads--;  //非守护线程数量减1
      daemon = false;

      // 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)  //当非守护线程数量为1时,唤醒在destroy_vm方法等待的线程(其实最后那个非守护线程就是销毁线程)
        Threads_lock->notify_all();
    }
    ThreadService::remove_thread(p, 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();

    // Now, this thread is not visible to safepoint
    p->set_safepoint_visible(false);
    // once the thread becomes safepoint invisible, we can not use its per-thread
    // recorder. And Threads::do_threads() no longer walks this thread, so we have
    // to release its per-thread recorder here.
    MemTracker::thread_exiting(p);
  } // unlock Threads_lock

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

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值