4

我有一些 Java 代码来创建关闭挂钩,以便在客户端按下 ctrl+C 时干净地退出:

private static void shutdownHandler(Thread mainThread) {
    try {
        mainThread.join(30000);
    } catch (InterruptedException e) {
    }
}

public static void main(String[] args) {
    final Thread mainThread = Thread.currentThread();
    Thread shutdownThread = new Thread(() -> shutdownHandler(mainThread));
    Runtime.getRuntime().addShutdownHook(shutdownThread);
}

当我从命令行运行它时,它按预期工作(主线程退出并几乎立即返回到命令提示符)。但是,如果我编写一个使用以下 C++ 代码调用它的 JNI 包装器:

JavaVMInitArgs vm_args;
// Populate vm_args

JavaVM *jvm;
JNIEnv *env;
JNI_CreateJavaVM(&jvm, reinterpret_cast<void**>(&env), &vm_args);

jclass mainClass = env->FindClass("path/to/my/class");
jmethod mainMethod = env->GetStaticMethodID(mainClass, "main", "([L" STRING_CLASS ";)V");

jclass stringClass = env->FindClass(STRING_CLASS);
jobjectArray mainArgs = env->NewObjectArray(0, stringClass, NULL);

env->CallStaticVoidMethod(mainClass, mainMethod, mainArgs);
jvm->DestroyJavaVM();

然后该shutdownHandler方法挂起,直到 30 秒超时,然后将控制权返回给 C++ 代码并最终退出。有谁知道shutdownHandler从 JNI 调用开始时允许该方法加入主线程的方法?

4

1 回答 1

6

在您的第一个示例中,主线程退出,然后 JVM 检测到没有非守护程序线程,并将启动 JVM 关闭。此时,加入主线程没有问题,因为它甚至在关闭之前就已经结束。

在您的第二个变体中,主线程,即main通过 执行该方法的线程env -> CallStaticVoidMethod(…)正忙于执行jvm -> DestroyJavaVM()。由于该函数等待关闭处理程序的完成,而您的关闭处理程序等待该线程的完成,因此您遇到了死锁。

您也可以使用纯 Java 代码获得类似的行为。当您放置System.exit(0);main方法的末尾时,让主线程启动关闭并等待其完成,您会遇到类似的死锁。

通常,您不应join在关闭处理程序中执行操作。这些处理程序应该尽快清理并返回。

或者,正如文档所说:

关闭挂钩在虚拟机生命周期的一个微妙时刻运行,因此应该进行防御性编码。特别是,它们应该写成线程安全的,并尽可能避免死锁。他们也不应该盲目依赖可能已经注册了自己的关闭钩子的服务,因此他们自己可能正在关闭过程中。例如,尝试使用其他基于线程的服务(例如 AWT 事件调度线程)可能会导致死锁。

于 2018-10-11T13:26:48.203 回答