목차
Background
问题排查
Java java지도 시간 Java System#exit가 프로그램을 종료할 수 없는 문제를 해결하는 방법

Java System#exit가 프로그램을 종료할 수 없는 문제를 해결하는 방법

Apr 25, 2023 am 09:55 AM
java

Background

친구가 상황에 직면했습니다: java.lang.System#exit가 애플리케이션을 종료할 수 없습니다.

이런 상황을 듣고 깜짝 놀랐습니다. 이 기능이 아직도 작동하나요? 궁금해지네요

그러다가 친구는 계속해서 장면 설명을 해줬습니다. Dubbo 애플리케이션이 등록 센터에 연결될 때 연결(타임아웃)에 실패하면 System#exit를 호출하여 애플리케이션을 종료해야 한다고 예상하지만 프로그램이 종료할 것으로 예상을 누르지 않으면 JVM 프로세스가 여전히 존재합니다

동시에 System#exit를 실행하는 코드를 다른 스레드에 넣으면 프로그램이 예상대로 종료되고 JVM 프로세스가 종료됩니다

의사 코드는 다음과 같이 설명됩니다.

Future<Object> future = 连接注册中心的Future;
try {
    Object o = future.get(3, TimeUnit.SECONDS);
} catch (Exception e) {
	log.error("connect failed xxxx");
    System.exit(1); // 程序无法退出
}

-----------

Future<Object> future = 连接注册中心的Future;
try {
    Object o = future.get(3, TimeUnit.SECONDS);
} catch (Exception e) {
	log.error("connect failed xxxx");
    new Thread(() -> System.exit(1)).start(); // 程序能按期望退出
}
로그인 후 복사

Friend 우리가 직면한 시나리오는 의사 코드에서 설명하는 것보다 훨씬 더 복잡하지만 우리가 직면하는 본질적인 문제는 동일합니다.

더 일반적인 질문은 Dubbo의 org.apache.dubbo.registry.zookeeper.ZookeeperRegistry#ZookeeperRegistry 생성자에서 System.exit(1);을 직접 실행하면 프로그램이 비동기식으로 종료될 수 없다는 것입니다. 스레드는 예상대로 종료될 수 있습니다System.exit(1);程序无法退出,放在异步线程中执行却可以按期望退出

即:

// org.apache.dubbo.registry.zookeeper.ZookeeperRegistry#ZookeeperRegistry

public ZookeeperRegistry(URL url, ZookeeperTransporter zookeeperTransporter) {
    super(url);
    System.exit(1); //JVM进程无法退出
    // ...(省略)
}

-----------
// org.apache.dubbo.registry.zookeeper.ZookeeperRegistry#ZookeeperRegistry

public ZookeeperRegistry(URL url, ZookeeperTransporter zookeeperTransporter) {
    super(url);
    new Thread(() -> {System.exit(1);}).start(); //JVM进程正常退出
    // ...(省略)
}
로그인 후 복사

这就更令人惊奇了!

问题排查

要找出问题产生的原因,首先得有一些预备知识,否则会茫然无措,感觉无从下手

  • java.lang.System#exit 方法是Java提供的能够停止JVM进程的方法

  • 该方法被触发时,JVM会去调用Shutdown Hook(关闭勾子)方法,直到所有勾子方法执行完毕,才会关闭JVM进程

由上述第2点猜测:是否存在死循环的勾子函数无法退出,以致JVM没有去关闭进程?

举个例子:

public static void main(String[] args) {
    Runtime.getRuntime().addShutdownHook(new Thread(() -> {
        while (true) {
            try {
                System.out.println("closing...");
                TimeUnit.SECONDS.sleep(1);
            } catch (InterruptedException e) {
            }
        }
    }));

    System.out.println("before exit...");
    System.exit(0); 
    System.out.println("after exit..."); //代码不会执行
}
로그인 후 복사

如上,在main方法里先注册了一个shutdown hook,该勾子函数是个死循环,永远也不会退出,每3秒打印一次"closing…"

接着执行System.exit(0);方法,期望退出JVM进程

before exit...
closing...
closing...
closing...
closing...
closing...

...

结果是控制台不断打印"closing…",且JVM进程没有退出

原因正是上述第二点储备知识提到的:JVM会等待所有勾子执行完毕之后,才关闭进程。而示例中的shutdown hook 永远也不会执行完毕,因此JVM进程也不会被关闭

尽管有了储备知识,仍然很疑惑:如果存在死循环的shutdown hook,那么System.exit无论是在主线程中调用,还是在异步线程中调用,都应该不会关闭JVM进程;反之,如果不存在死循环的shutdown hook,无论是在哪个线程调用,都应该关闭JVM进程。为什么在背景的伪代码中,却是因为不同的调用线程执行System.exit,导致不一样的结果呢?

这时候只好想办法,看看shutdown hook们都在偷摸干啥事,为什么未执行完毕,以致JVM进程不能退出

恰好对Dubbo的源码也略有研究,很容易就找到org.apache.dubbo.registry.zookeeper.ZookeeperRegistry#ZookeeperRegistry的构造函数,并在其中加上一行代码,如下所示,改完之后重新编译源码,并引入自己的工程中进行Debug

注:本次使用的Dubbo版本为2.7.6

// org.apache.dubbo.registry.zookeeper.ZookeeperRegistry#ZookeeperRegistry
public ZookeeperRegistry(URL url, ZookeeperTransporter zookeeperTransporter) {
    super(url);
    System.exit(1); // 新增加的一行代码
    // ...(省略)
}
로그인 후 복사

启动工程,熟悉Dubbo的朋友应该会知道,应用启动的过程中会去注册中心(这儿是Zookeeper)注册或者订阅,因为启动的是消费者,因此应用会尝试连接注册中心Zookeeper,会走到ZookeeperRegistry的构造函数,由于构造函数第二行是新增的代码System.exit(1);按照背景的说法,JVM不会退出,且会卡死,这时候,借助IDEA的"快照"功能,可以"拍"下Java线程栈的运行情况,功能上相当于执行jstack命令

Java System#exit가 프로그램을 종료할 수 없는 문제를 해결하는 방법

Java System#exit가 프로그램을 종료할 수 없는 문제를 해결하는 방법

从线程栈中看出一个可疑的线程:DubboShutdownHook

从名字上可以看出是一个Dubbo注册的一个shutdown hook,其主要目的是为了关闭连接、做一些资源的回收等工作

从图中也可以看出,线程阻塞在org.apache.dubbo.registry.support.AbstractRegistryFactory

즉,

public static void destroyAll() {
    if (!destroyed.compareAndSet(false, true)) {
        return;
    }

    if (LOGGER.isInfoEnabled()) {
        LOGGER.info("Close all registries " + getRegistries());
    }
    // Lock up the registry shutdown process
    LOCK.lock(); // 83行,DubboShutdownHook线程阻塞在此处
    try {
        for (Registry registry : getRegistries()) {
            try {
                registry.destroy();
            } catch (Throwable e) {
                LOGGER.error(e.getMessage(), e);
            }
        }
        REGISTRIES.clear();
    } finally {
        // Release the lock
        LOCK.unlock();
    }
}
로그인 후 복사

이것은 훨씬 더 놀랍습니다!

문제 해결

문제의 원인을 찾으려면 먼저 몇 가지 준비 지식이 있어야 합니다. 그렇지 않으면 헤매고 시작할 수 없는 느낌을 받게 됩니다🎜
  • 🎜java.lang .System#exit 메소드는 JVM 프로세스를 중지할 수 있는 Java에서 제공하는 메소드입니다.🎜
  • 🎜이 메소드가 트리거되면 JVM은 모든 후크 메소드가 종료될 때까지 Shutdown Hook 메소드를 호출합니다. , JVM 프로세스가 종료됩니까🎜
🎜위의 2번에서 추측해 보세요. 🎜JVM이 프로세스를 종료하지 않도록 종료할 수 없는 무한 루프 후크 기능이 있습니까? 🎜🎜🎜🎜예: 🎜🎜
org.apache.dubbo.registry.support.AbstractRegistryFactory#getRegistry(org.apache.dubbo.common.URL)
로그인 후 복사
🎜 위와 같이 종료 후크가 기본 메소드에 등록됩니다. 이 후크 기능은 무한 루프이며 3초마다 "closing..."을 인쇄하지 않습니다. System.exit(0); 메소드를 실행하고 종료하기 전에 JVM 프로세스🎜
🎜를 종료할 것으로 예상합니다...
닫는 중...
닫는 중...
닫는 중...
닫는 중...
닫는 중...🎜🎜...🎜
🎜결과적으로 콘솔은 "closing…"을 계속 인쇄하고 JVM은 프로세스가 종료되지 않습니다🎜 🎜이유는 위의 예비 지식의 두 번째 항목에서 언급된 것과 정확히 같습니다: JVM은 프로세스를 닫기 전에 모든 후크가 실행될 때까지 기다립니다. 예제의 종료 후크는 절대 실행되지 않으므로 JVM 프로세스는 종료되지 않습니다🎜🎜 축적된 지식에도 불구하고 여전히 혼란스럽습니다. 무한 루프 종료 후크가 있는 경우 System.exit</ code>메인 스레드에서 호출되든 비동기 스레드에서 호출되든 상관없이 JVM 프로세스를 종료해서는 안 됩니다🎜반대로, 무한 루프 종료 후크가 없으면 호출되는 스레드에 관계없이 JVM 프로세스를 종료해야 합니다. 🎜 JVM 프로세스를 닫습니다. 🎜Background🎜의 의사 코드에서 서로 다른 호출 스레드가 <code>System.exit를 실행하여 결과가 달라지는 이유는 무엇입니까? 🎜🎜이때 셧다운 후크가 비밀리에 무엇을 하는지, 왜 완료되지 않고 JVM 프로세스가 종료되지 않는지 확인할 수 있는 방법을 생각해야 했습니다. 🎜🎜 우연히 소스 코드에 대해 조사를 하게 되었습니다. Dubbo의 org.apache를 쉽게 찾았습니다. dubbo.registry.zookeeper.ZookeeperRegistry#ZookeeperRegistry에 코드 한 줄을 추가하고 변경 후 소스 코드를 다시 컴파일하여 자신만의 코드로 도입합니다. 디버깅을 위한 프로젝트🎜
🎜참고: 이 용도는 Dubbo 버전이 2.7.6🎜
// org.apache.dubbo.registry.support.AbstractRegistryFactory

public Registry getRegistry(URL url) {
    // ...(省略)
    LOCK.lock(); // 获取锁
    try {
        // ...(省略)
        // 创建Registry,由于我们选用的注册中心是Zookeeper,因此通过SPI选择了ZookeeperRegistryFactory对ZookeeperRegistry进行创建,最终会调用到我们添加过一行System.exit的ZookeeperRegistry构造函数中
        
        registry = createRegistry(url); 
        
        // ...(省略)
    } finally {
        // Release the lock
        LOCK.unlock(); // 创建完registry,与注册中心连上之后,才会释放锁
    }
}
로그인 후 복사
로그인 후 복사
🎜입니다. 프로젝트를 시작하세요. Dubbo에 익숙한 친구들은 애플리케이션 시작 과정에서 등록 또는 구독을 위해 등록 센터(여기서는 Zookeeper)로 이동합니다. 왜냐하면 시작되는 소비자이기 때문입니다. 따라서 애플리케이션은 등록 센터 Zookeeper에 연결을 시도하고 ZookeeperRegistry</code의 생성자로 이동합니다. > 생성자의 두 번째 줄은 🎜Background🎜에 따라 새로 추가된 코드 <code>System.exit(1);이므로 이때 JVM이 종료되지 않고 중단될 것이라고 합니다. IDEA의 "스냅샷" 기능을 사용하면 Java 스레드 스택의 실행 상태를 "사진으로 찍을" 수 있습니다. 이는 jstack</ code>Command🎜🎜<img src="https://img 실행과 기능적으로 동일합니다. .php.cn/upload/article/000/887/227/168238771440471.png" alt="Java System#exit가 프로그램을 종료할 수 없는 문제를 해결하는 방법" / >🎜🎜<img src="https:// img.php.cn/upload/article/000/887/227/168238771474546.jpg" alt="Java System#exit가 프로그램을 종료할 수 없는 문제를 해결하는 방법" />🎜 🎜에서 의심스러운 스레드가 보입니다. 스레드 스택: 🎜DubboShutdownHook🎜🎜🎜이름에서 알 수 있듯이 Dubbo에 의해 등록된 종료 후크입니다. 주요 목적은 연결을 닫고 일부 리소스 재활용을 수행하는 것입니다. 그림에서도 볼 수 있습니다. 스레드가 <code>org.apache.dubbo.registry.support.AbstractRegistryFactory🎜
// org.apache.dubbo.registry.zookeeper.ZookeeperRegistryFactory

public Registry createRegistry(URL url) {
	// 调用修改过源码的ZookeeperRegistry构造函数
    return new ZookeeperRegistry(url, zookeeperTransporter);
}
로그인 후 복사
로그인 후 복사
🎜의 83번째 줄에서 차단되었다는 것은 코드에서 잠금을 얻을 수 없다는 것이 명백하므로 스레드는 다음 줄에서 차단됩니다. 83, 잠금을 획득하기 위해 대기 중입니다. 즉, 잠금을 보유하고 있는 다른 스레드가 있지만 아직 해제되지 않았습니다. DubboShutdownHook은 기다려야 합니다.🎜🎜아래 그림과 같이 IDEA를 사용하여 잠금을 획득했습니다. 🎜
// java.lang.System

public static void exit(int status) {
    Runtime.getRuntime().exit(status);
}
로그인 후 복사
로그인 후 복사
🎜를 찾으면 자물쇠🎜를 얻을 수 있습니다.
// org.apache.dubbo.registry.support.AbstractRegistryFactory

public Registry getRegistry(URL url) {
    // ...(省略)
    LOCK.lock(); // 获取锁
    try {
        // ...(省略)
        // 创建Registry,由于我们选用的注册中心是Zookeeper,因此通过SPI选择了ZookeeperRegistryFactory对ZookeeperRegistry进行创建,最终会调用到我们添加过一行System.exit的ZookeeperRegistry构造函数中
        
        registry = createRegistry(url); 
        
        // ...(省略)
    } finally {
        // Release the lock
        LOCK.unlock(); // 创建完registry,与注册中心连上之后,才会释放锁
    }
}
로그인 후 복사
로그인 후 복사
// org.apache.dubbo.registry.zookeeper.ZookeeperRegistryFactory

public Registry createRegistry(URL url) {
	// 调用修改过源码的ZookeeperRegistry构造函数
    return new ZookeeperRegistry(url, zookeeperTransporter);
}
로그인 후 복사
로그인 후 복사

如此,System.exit无法退出JVM进程的问题总算真相大白了:

1.Dubbo启动过程中会先获取锁,然后创建registry与注册中心进行连接,在ZookeeperRegistry中调用了java.lang.System#exit方法,程序转而执行"唤起shutdown hook"的代码并阻塞等待所有勾子函数执行完毕,而此时,之前持有的锁并没有释放

2.所有勾子函数(每个勾子函数都对应一个线程)被唤醒并执行,其中有一个Dubbo的勾子函数在执行的过程中,需要获取步骤1中的锁,由于获取锁失败,就阻塞等待着

3.由于1没有释放锁的情况下等待2执行完,而2的执行需要等待1释放锁,这样就形成了一个类似"死锁"的场景,因此也就导致了程序卡死,而JVM进程还存活的现象。之所以称为"类似"死锁,是因为1中执行System.exit的线程,也即持有锁的线程,永远不会走到释放锁的代码:一旦程序进入System.exit的世界里,就像进了一个单向虫洞,只能进不能出,如果勾子函数执行完毕,JVM进程接着就会被关闭,不会有机会再释放锁

那么,为什么在异步线程中执行System.exit,却能够正常退出JVM?

那是因为:"唤起shutdown hook"并阻塞等待所有勾子函数执行完毕的线程是其它线程(此处假设是线程A),该线程在阻塞时并未持有任何锁,而主线程会继续往下执行并接着释放锁。一旦锁释放,Shutdown hook就有机会持有该锁,并且执行其它资源的回收操作,等到所有的shutdown hook执行完毕,A线程就能从阻塞中返回并执行halt方法关闭JVM,因此能够正常退出JVM进程

深入学习

以上是对java.lang.System#exit 无法退出程序问题的分析,来龙去脉已经阐述清楚,受益于对Dubbo源码的了解以及正确的排查思路和排查手段,整个问题排查过程其实并没有花太多时间,但可以趁着这个机会,把java.lang.System#exit系统学习一下,或许会对以后问题排查、基础组件设计提供一些思路

System#exit

// java.lang.System

public static void exit(int status) {
    Runtime.getRuntime().exit(status);
}
로그인 후 복사
로그인 후 복사

Terminates the currently running Java Virtual Machine. The argument serves as a status code; by convention, a nonzero status code indicates abnormal termination.
This method calls the exit method in class Runtime. This method never returns normally.
The call System.exit(n) is effectively equivalent to the call:
Runtime.getRuntime().exit(n)

这个方法实现非常简单,是Runtime#exit的一个简便写法,其作用是用来关闭JVM进程,一旦调用该方法,永远也不会从该方法正常返回:执行完该方法后JVM进程就直接关闭了。

入参status取值分两类:0值与非0值,0值意味着正常关闭,非0值意味着异常关闭。

传入0值[有可能]会去执行所有的finalizer方法,非0值则一定不会执行(都不正常了,还执行啥finalizer呢?)。这儿提及[有可能]是因为,默认并不会执行finalizers,需要调用java.lang.Runtime#runFinalizersOnExit方法开启,而该方法早被JDK标识为Deprecated,因此通常情况下是不会开启的

// java.lang.Runtime

@Deprecated
public static void runFinalizersOnExit(boolean value) {
    SecurityManager security = System.getSecurityManager();
    if (security != null) {
        try {
            security.checkExit(0);
        } catch (SecurityException e) {
            throw new SecurityException("runFinalizersOnExit");
        }
    }
    Shutdown.setRunFinalizersOnExit(value);
}
로그인 후 복사

接着看java.lang.Runtime#exit,可以看到,最终调用的是Shutdown.exit(status);,该方法是个包级别可见的方法,外部不可见

// java.lang.Runtime

public void exit(int status) {
    SecurityManager security = System.getSecurityManager();
    if (security != null) {
        security.checkExit(status);
    }
    Shutdown.exit(status);
}
로그인 후 복사
// java.lang.Shutdown

static void exit(int status) {
    // ...(省略)
    synchronized (Shutdown.class) {
        /* Synchronize on the class object, causing any other thread
         * that attempts to initiate shutdown to stall indefinitely
         */
        // 执行shutdown序列
        sequence();
        // 关闭JVM
        halt(status);
    }
}
로그인 후 복사
// java.lang.Shutdown

private static void sequence() {
    // ...(省略)
    runHooks();
    // ...(省略)
}
로그인 후 복사
// java.lang.Shutdown

private static void runHooks() {
    for (int i=0; i < MAX_SYSTEM_HOOKS; i++) {
        try {
            Runnable hook;
            synchronized (lock) {
                // 这个锁很重要,目的是通过Happens-Before保证内存的可见性
                currentRunningHook = i;
                hook = hooks[i];
            }
            if (hook != null) hook.run(); //执行勾子函数
        } catch(Throwable t) {
            if (t instanceof ThreadDeath) {
                ThreadDeath td = (ThreadDeath)t;
                throw td;
            }
        }
    }
}
로그인 후 복사

java.lang.Shutdown#runHooks有两个点需要注意,第一点MAX_SYSTEM_HOOKS(hooks)这个并不是我们注册的shutdown hooks,而是按顺序预定义的系统关闭勾子,目前JDK源码(JDK8)预定义了三个:

  • Console restore hook

  • Application hooks

  • DeleteOnExit hook

其中,Application hooks才是我们应用程序中主动注册的shutdown hook。

java.lang.ApplicationShutdownHooks类初始化时,会执行static代码块,并在其中注册了Application hooks

// java.lang.ApplicationShutdownHooks

class ApplicationShutdownHooks {
    /* The set of registered hooks */
    // 这个才是我们应用程序代码中注册的shutdown hook
    private static IdentityHashMap<Thread, Thread> hooks; 
    static {
        try {
            Shutdown.add(1 /* shutdown hook invocation order */,
                false /* not registered if shutdown in progress */,
                new Runnable() {
                    public void run() {
                        runHooks();
                    }
                }
            );
            hooks = new IdentityHashMap<>();
        } catch (IllegalStateException e) {
            // application shutdown hooks cannot be added if
            // shutdown is in progress.
            hooks = null;
        }
    }
로그인 후 복사

其次要注意的点是,给hook变量赋值的时候进行了加锁

Runnable hook;
synchronized (lock) {
    currentRunningHook = i;
    hook = hooks[i];
}
로그인 후 복사

一般而言,给局部变量赋值是不需要加锁的,因为局部变量是栈上变量,而线程栈之间数据是隔离的,不会出现线程安全的问题,因此不需要靠加锁来保证数据并发访问的安全性。

而此处加锁也并非为了解决线程安全问题,其真正的目的在于,通过Happens-Before规则来保证hooks的内存可见性:An unlock on a monitor happens-before every subsequent lock on that monitor。

如果不加锁,有可能导致从hooks数组中读取到的值并不是内存中最新的变量值,而是一个旧值

上面是读取hooks数组给hook变量赋值,为了满足HB(Happens-Before)原则,需要确保写操作中同样对hooks变量进行了加锁,因此我们看一下写hooks数组的地方,如下:

// java.lang.Shutdown

static void add(int slot, boolean registerShutdownInProgress, Runnable hook) {
    synchronized (lock) {
    		// ...(省略)
        hooks[slot] = hook;
    }
}
로그인 후 복사

操作确实加了锁,这样才能让接下来的 操作的加锁行为满足HB原则

由于篇幅原因,就不展开具体的HB介绍,相信了解过HB原则的朋友一下就能明白其中的原理

这个点个人感觉很有意思,因为锁的作用不单是为了保证线程安全,还可以用来做为内存通信、保证内存可见性的手段,因此可以当作面试的一个点,当下次面试官问到:你写的代码中用过锁(synchronized)吗?什么场景用到锁?都集群部署了,单机锁还有意义吗? 我们就可以回答:为了保证内存的可见性,balabalaba

所以你瞧,这个点其实也给我们设计基础组件带来很大的启发,synchronized在当今集群、分布式环境下并非一无是处,总有合适的地方在等待着它发挥光和热

注:JDK源码中真处处是宝藏,很多地方隐藏着巧妙而不可缺少的设计

在给hook变量赋值之后,就执行 if (hook != null) hook.run();,其中会执行到Application hooks,即上面提到的在ApplicationShutdownHooks类初始化时注册的勾子,勾子内部调用了java.lang.ApplicationShutdownHooks#runHooks方法

// java.lang.ApplicationShutdownHooks

Shutdown.add(1 /* shutdown hook invocation order */,
    false /* not registered if shutdown in progress */,
    new Runnable() {
        public void run() {
            runHooks();
        }
    }
);
로그인 후 복사
// java.lang.ApplicationShutdownHooks

static void runHooks() {
    Collection<Thread> threads;
    synchronized(ApplicationShutdownHooks.class) {
        threads = hooks.keySet(); // hooks才是应用程序真正注册的shutdown hook
        hooks = null;
    }
		// 每一个shutdown hook都对应一个thread,由此可见是并发执行关闭勾子函数
    for (Thread hook : threads) {
        hook.start();
    }
    for (Thread hook : threads) {
        while (true) {
            try {
                hook.join(); // 死等到hook执行完毕
                break;
            } catch (InterruptedException ignored) {
                // 即便被唤醒都不搭理,接着进行下一轮循环,继续死等
            }
        }
    }
}
로그인 후 복사

上面的hooks才是应用程序真正注册的shutdown hook,由源码可以看出,每一个hook都对应着一个thread,且调用了它们的start方法,即开启thread,意味着shutdown hook是并发无序地执行

接着,唤起shutdown hook的线程,会通过死循环和join死等到所有关闭勾子都执行完毕,且忽略任何 唤醒异常。也即是说,如果勾子们不执行完,唤醒线程是不会离开的

等所有的Application hooks执行完毕,接下来会执行DeleteOnExit hook(如果存在),等所有system hooks执行完毕,也基本意味着sequence方法执行完毕,接下来就执行halt方法关闭JVM虚拟机

synchronized (Shutdown.class) {
    sequence();
    halt(status);
}
로그인 후 복사

这里额外还有一个知识点,上文只是提了一嘴,可能会容易忽略,此处拿出来解释一下:执行java.lang.System#exit永远也不会从该方法正常返回,也即是说,即便System#exit后边跟着的是finally,也不会执行 。一不注意就容易掉坑里

try {
    // ...
    System.exit(0);
} finally {
    // 这里的代码永远执行不到
}
로그인 후 복사

java.lang.Runtime#addShutdownHook

聊完System#exit方法,接着来聊聊注册shutdown hook的方法。该方法本身实现上很简单,如下示:

// java.lang.Runtime
public void addShutdownHook(Thread hook) {
    // ...(省略)
    ApplicationShutdownHooks.add(hook);
}

// java.lang.ApplicationShutdownHooks
static synchronized void add(Thread hook) {
    // ...(省略)
    hooks.put(hook, hook);
}
로그인 후 복사

需要注意的是,注册的关闭勾子会在以下几种时机被调用到

程序正常退出

  • 最后一个非守护线程执行完毕退出时

  • System.exit方法被调用时

程序响应外部事件

  • 程序响应用户输入事件,例如在控制台按ctrl+c(^+c)

  • 程序响应系统事件,如用户注销、系统关机等

除此之外,shutdown hook是不会被执行的

Shutdown hook存在的意义之一,是能够帮助我们实现优雅停机,而优雅停机的意义是:应用的重启、停机等操作,不影响业务的连续性

以Dubbo Provider的视角为例,优雅停机需要满足两点基本诉求:

  • Consumer不应该请求到已经下线的Provider

  • 在途请求需要处理完毕,不能被停机指令中断

Dubbo注册了Shutdown hook,JVM在收到操作系统发来的关闭指令时,会执行关闭勾子

  • 在勾子中停止与注册中心的连接,注册中心会通知Consumer某个Provider已下线,后续不应该再调用该Provider进行服务。此行为是断掉上游流量,满足第一点诉求

  • 接着,勾子执行Protocol(Dubbo相关概念)的注销逻辑,在其中判断server(Dubbo相关概念)是否还在处理请求,在超时时间内等待所有任务处理完毕,则关闭server。此行为是处理在途请求,满足第二点述求

因此,一种优雅停机的整体方案如下:

$pid = ps | grep xxx // 查找要关闭的应用
kill $pid // 发出关闭应用指令
sleep for a period of time // 等待一段时间,让应用程序执行shutdown hook进行现场的保留跟资源的清理工作

$pid = ps | grep xxx // 再次查找要关闭的应用,如果还存在,就需要强行关闭应用
if($pid){kill -9 $pid} // 等待一段时间之后,应用程序仍然没有正常停止,则需要强行关闭应用
로그인 후 복사

위 내용은 Java System#exit가 프로그램을 종료할 수 없는 문제를 해결하는 방법의 상세 내용입니다. 자세한 내용은 PHP 중국어 웹사이트의 기타 관련 기사를 참조하세요!

본 웹사이트의 성명
본 글의 내용은 네티즌들의 자발적인 기여로 작성되었으며, 저작권은 원저작자에게 있습니다. 본 사이트는 이에 상응하는 법적 책임을 지지 않습니다. 표절이나 침해가 의심되는 콘텐츠를 발견한 경우 admin@php.cn으로 문의하세요.

핫 AI 도구

Undresser.AI Undress

Undresser.AI Undress

사실적인 누드 사진을 만들기 위한 AI 기반 앱

AI Clothes Remover

AI Clothes Remover

사진에서 옷을 제거하는 온라인 AI 도구입니다.

Undress AI Tool

Undress AI Tool

무료로 이미지를 벗다

Clothoff.io

Clothoff.io

AI 옷 제거제

AI Hentai Generator

AI Hentai Generator

AI Hentai를 무료로 생성하십시오.

인기 기사

R.E.P.O. 에너지 결정과 그들이하는 일 (노란색 크리스탈)
4 몇 주 전 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O. 최고의 그래픽 설정
4 몇 주 전 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O. 아무도들을 수없는 경우 오디오를 수정하는 방법
4 몇 주 전 By 尊渡假赌尊渡假赌尊渡假赌
R.E.P.O. 채팅 명령 및 사용 방법
4 몇 주 전 By 尊渡假赌尊渡假赌尊渡假赌

뜨거운 도구

메모장++7.3.1

메모장++7.3.1

사용하기 쉬운 무료 코드 편집기

SublimeText3 중국어 버전

SublimeText3 중국어 버전

중국어 버전, 사용하기 매우 쉽습니다.

스튜디오 13.0.1 보내기

스튜디오 13.0.1 보내기

강력한 PHP 통합 개발 환경

드림위버 CS6

드림위버 CS6

시각적 웹 개발 도구

SublimeText3 Mac 버전

SublimeText3 Mac 버전

신 수준의 코드 편집 소프트웨어(SublimeText3)

자바의 완전수 자바의 완전수 Aug 30, 2024 pm 04:28 PM

Java의 완전수 가이드. 여기서는 정의, Java에서 완전 숫자를 확인하는 방법, 코드 구현 예제에 대해 논의합니다.

Java의 난수 생성기 Java의 난수 생성기 Aug 30, 2024 pm 04:27 PM

Java의 난수 생성기 안내. 여기서는 예제를 통해 Java의 함수와 예제를 통해 두 가지 다른 생성기에 대해 설명합니다.

자바의 웨카 자바의 웨카 Aug 30, 2024 pm 04:28 PM

Java의 Weka 가이드. 여기에서는 소개, weka java 사용 방법, 플랫폼 유형 및 장점을 예제와 함께 설명합니다.

Java의 스미스 번호 Java의 스미스 번호 Aug 30, 2024 pm 04:28 PM

Java의 Smith Number 가이드. 여기서는 정의, Java에서 스미스 번호를 확인하는 방법에 대해 논의합니다. 코드 구현의 예.

Java Spring 인터뷰 질문 Java Spring 인터뷰 질문 Aug 30, 2024 pm 04:29 PM

이 기사에서는 가장 많이 묻는 Java Spring 면접 질문과 자세한 답변을 보관했습니다. 그래야 면접에 합격할 수 있습니다.

Java 8 Stream foreach에서 나누거나 돌아 오시겠습니까? Java 8 Stream foreach에서 나누거나 돌아 오시겠습니까? Feb 07, 2025 pm 12:09 PM

Java 8은 스트림 API를 소개하여 데이터 컬렉션을 처리하는 강력하고 표현적인 방법을 제공합니다. 그러나 스트림을 사용할 때 일반적인 질문은 다음과 같은 것입니다. 기존 루프는 조기 중단 또는 반환을 허용하지만 스트림의 Foreach 메소드는이 방법을 직접 지원하지 않습니다. 이 기사는 이유를 설명하고 스트림 처리 시스템에서 조기 종료를 구현하기위한 대체 방법을 탐색합니다. 추가 읽기 : Java Stream API 개선 스트림 foreach를 이해하십시오 Foreach 메소드는 스트림의 각 요소에서 하나의 작업을 수행하는 터미널 작동입니다. 디자인 의도입니다

Java의 날짜까지의 타임스탬프 Java의 날짜까지의 타임스탬프 Aug 30, 2024 pm 04:28 PM

Java의 TimeStamp to Date 안내. 여기서는 소개와 예제와 함께 Java에서 타임스탬프를 날짜로 변환하는 방법에 대해서도 설명합니다.

캡슐의 양을 찾기위한 Java 프로그램 캡슐의 양을 찾기위한 Java 프로그램 Feb 07, 2025 am 11:37 AM

캡슐은 3 차원 기하학적 그림이며, 양쪽 끝에 실린더와 반구로 구성됩니다. 캡슐의 부피는 실린더의 부피와 양쪽 끝에 반구의 부피를 첨가하여 계산할 수 있습니다. 이 튜토리얼은 다른 방법을 사용하여 Java에서 주어진 캡슐의 부피를 계산하는 방법에 대해 논의합니다. 캡슐 볼륨 공식 캡슐 볼륨에 대한 공식은 다음과 같습니다. 캡슐 부피 = 원통형 볼륨 2 반구 볼륨 안에, R : 반구의 반경. H : 실린더의 높이 (반구 제외). 예 1 입력하다 반경 = 5 단위 높이 = 10 단위 산출 볼륨 = 1570.8 입방 단위 설명하다 공식을 사용하여 볼륨 계산 : 부피 = π × r2 × h (4

See all articles