即使主线程执行完毕,Java 应用程序如何继续运行?

Ani*_*esh 2 java multithreading thread-safety java-threads

当应用程序通过 Java 中的 main 方法启动并为以下任务旋转其他用户线程(不是守护进程)时:

  • 用户线程 01:从数据库加载缓存
  • User Thread-02:执行应用程序启动任务,如运行诊断
  • User Thread-03 : 杂项任务

由主线程启动的用户线程将在某个时间点完成执行并终止。这将导致主线程最终终止,应用程序将停止运行。但是,正如我们看到许多曾经开始运行的应用程序一样,如果我们进行线程转储,我们可以在最底部看到主线程。这意味着主线程不会终止,这意味着启动的用户线程不会终止。

这是如何实现的?我的意思是什么是标准的编程结构和逻辑来保持线程处于活动状态而不通过无限 for 或 while 循环运行它们?

感谢您通过这个问题。每一个有用的回复都会增加我们的知识。

Bas*_*que 6

执行器框架

你说:

旋转其他用户线程

希望您不是Thread直接寻址对象。从 Java 5 开始,在大多数情况下,我们可以使用 Executors 框架来管理后台线程上的工作。请参阅Oracle 的教程

ExecutorService es = Executors. … ;
es.submit( yourRunnableOrCallableHere ) ;  // Optional: Capture the returned `Future` object to track success/failure of your task.
…
es.shutdown() ;
Run Code Online (Sandbox Code Playgroud)

后台线程结束对主线程没有影响

你说:

由主线程启动的用户线程将在某个时间点完成执行并终止。这将导致主线程最终终止,应用程序将停止运行。

不正确。

主线程在没有更多工作要做时结束。后台线程可能在主线程之前结束,也可以在主线程之后结束。在后台线程终止并不会导致主线程结束。

下面是一些示例代码来演示此行为。此应用程序执行线程转储,然后在后台运行一个任务,该任务也执行线程转储。主线程和后台线程都会休眠几秒钟。

package work.basil.example;

import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class Threading
{
    public static void main ( String[] args )
    {
        Threading app = new Threading();
        app.demo();
    }

    private void demo ( )
    {
        System.out.println( "---------------|  main thread  |------------------------------------" );
        System.out.println( "Bonjour. " + Instant.now() );
        System.out.println( Threading.threadDump( true , true ) );

        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.submit(
                ( ) -> {
                    System.out.println( "---------------|  background thread  |------------------------------------" );
                    System.out.println( "DEBUG - Starting background thread. " + Instant.now() );
                    System.out.println( "DEBUG - Sleeping background thread. " + Instant.now() );
                    try {Thread.sleep( Duration.ofSeconds( 2 ).toMillis() );} catch ( InterruptedException e ) {e.printStackTrace();}
                    System.out.println( Threading.threadDump( true , true ) );
                    System.out.println( "DEBUG - Ending background thread. " + Instant.now() );
                }
        );

        executorService.shutdown();  // Always be sure to shutdown  your executor services.
        try {Thread.sleep( Duration.ofSeconds( 5 ).toMillis() );} catch ( InterruptedException e ) {e.printStackTrace();}
        System.out.println( "---------------|  main thread  |------------------------------------" );
        System.out.println( Threading.threadDump( true , true ) );
        System.out.println( "DEBUG - Main thread ending. " + Instant.now() );
    }

    // `threadDump` method taken from: https://www.baeldung.com/java-thread-dump
    private static String threadDump ( boolean lockedMonitors , boolean lockedSynchronizers )
    {
        StringBuffer threadDump = new StringBuffer( System.lineSeparator() );
        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
        for ( ThreadInfo threadInfo : threadMXBean.dumpAllThreads( lockedMonitors , lockedSynchronizers ) )
        {
            String message = "Thread: " + threadInfo.getThreadId() + " | " + threadInfo.getThreadName();
            threadDump.append( message ).append( System.lineSeparator() );
//            threadDump.append( threadInfo.toString() );
        }
        return threadDump.toString();
    }
}
Run Code Online (Sandbox Code Playgroud)

当我们使后台线程休眠的时间少于主线程时(2 秒对 5 秒),请注意主线程继续运行。后台线程结束对主线程继续或结束没有影响。

运行时,请注意如何使用已提交的任务启动执行程序服务会导致这里又有两个 ID 为 14 和 15 的线程。然后后台任务结束,executor服务关闭后,ID为14的线程就消失了。注意主线程如何不结束,继续工作——与您在问题中的陈述相矛盾。

package work.basil.example;

import java.lang.management.ManagementFactory;
import java.lang.management.ThreadInfo;
import java.lang.management.ThreadMXBean;
import java.time.Duration;
import java.time.Instant;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.ScheduledExecutorService;
import java.util.concurrent.TimeUnit;

public class Threading
{
    public static void main ( String[] args )
    {
        Threading app = new Threading();
        app.demo();
    }

    private void demo ( )
    {
        System.out.println( "---------------|  main thread  |------------------------------------" );
        System.out.println( "Bonjour. " + Instant.now() );
        System.out.println( Threading.threadDump( true , true ) );

        ExecutorService executorService = Executors.newCachedThreadPool();
        executorService.submit(
                ( ) -> {
                    System.out.println( "---------------|  background thread  |------------------------------------" );
                    System.out.println( "DEBUG - Starting background thread. " + Instant.now() );
                    System.out.println( "DEBUG - Sleeping background thread. " + Instant.now() );
                    try {Thread.sleep( Duration.ofSeconds( 2 ).toMillis() );} catch ( InterruptedException e ) {e.printStackTrace();}
                    System.out.println( Threading.threadDump( true , true ) );
                    System.out.println( "DEBUG - Ending background thread. " + Instant.now() );
                }
        );

        executorService.shutdown();  // Always be sure to shutdown  your executor services.
        try {Thread.sleep( Duration.ofSeconds( 5 ).toMillis() );} catch ( InterruptedException e ) {e.printStackTrace();}
        System.out.println( "---------------|  main thread  |------------------------------------" );
        System.out.println( Threading.threadDump( true , true ) );
        System.out.println( "DEBUG - Main thread ending. " + Instant.now() );
    }

    // `threadDump` method taken from: https://www.baeldung.com/java-thread-dump
    private static String threadDump ( boolean lockedMonitors , boolean lockedSynchronizers )
    {
        StringBuffer threadDump = new StringBuffer( System.lineSeparator() );
        ThreadMXBean threadMXBean = ManagementFactory.getThreadMXBean();
        for ( ThreadInfo threadInfo : threadMXBean.dumpAllThreads( lockedMonitors , lockedSynchronizers ) )
        {
            String message = "Thread: " + threadInfo.getThreadId() + " | " + threadInfo.getThreadName();
            threadDump.append( message ).append( System.lineSeparator() );
//            threadDump.append( threadInfo.toString() );
        }
        return threadDump.toString();
    }
}
Run Code Online (Sandbox Code Playgroud)

为了好玩,试试那个代码,但颠倒持续时间。使用 5 秒与 2 秒来查看后台线程比主线程更长寿。

网络服务器一直忙于监听

你在评论中说:

像这样想象它......我们有一个网站,即使没有人在浏览器中打开网页......这意味着即使没有人与之交互,该应用程序也在运行......如果我们说它是在后台运行的 Web 服务器,而不是实际的 Web 应用程序……但是,即使没有人与之交互,Web 应用程序如何无限期地运行。

关于“即使没有人在浏览器中打开网页,我们也有一个活跃的网站”,不,您的网站不是“活跃的”。如果没有任何待处理的 HTTP 请求要处理,您的 Java Servlet 就不会执行。您的 servlet 已加载并初始化,但在请求到达之前不会执行。

关于“这意味着即使没有人与之交互,应用程序也在运行”,正如我上面所说,没有您的网络应用程序没有运行。您的 Java servlet 代码没有执行。当请求到达时,Web 容器会自动在线程中调用 servlet 的代码。最终,该 servlet 代码会生成作为响应发送回 Web 浏览器的内容。您的 servlet 代码的执行结束。用于该执行的线程要么结束,要么返回到线程池(由您的 Web 容器做出的内部决定)。为了简单起见,我忽略了推送技术WebSockets

关于:“如果我们说它是在后台运行的 Web 服务器而不是实际的 Web 应用程序”,则 Web 服务器始终运行一个额外的线程来侦听传入的请求。

? 这可能是您困惑的根源:Web 服务器总是很忙,忙着侦听传入的连接,无论是否执行您的 Java servlet 代码。

  • Web 服务器有一个线程专用于与主机操作系统一起工作,以侦听网络上的传入连接。
  • Web 服务器根据需要启动其他线程以通过制定和发送响应来响应请求。

关于:“即使没有人与之交互,Web 应用程序如何无限期地运行”,您忘记了主机操作系统正在与 Web 容器交互,无论用户是否与您的 Web 应用程序交互。Web 容器维护一个线程,侦听传入的连接。该线程处于阻塞状态,等待主机操作系统网络堆栈对传入请求的通知。(我在这里的描述是概括和简化的——我不是网络专家——但足以说明这里的要点。)

当请求通过网络传入时,主机操作系统会通知 Web 容器。此通知解除阻塞侦听线程。侦听线程将请求分派给新线程,从而执行 Java servlet 的代码。同时,Web 容器的请求侦听线程又回到被阻塞状态,以等待来自主机操作系统网络堆栈的关于另一个传入请求的另一个通知。

那个被阻塞的监听线程解释/启用了网络服务器连续和无限期地运行。相比之下,您的 Web 应用程序会突然运行,仅响应请求。

您的问题归功于 Java Servlet 技术的天才和成功。Java Servlet 的真正目的是抽象出有关侦听网络活动、将数据包转换为文本以解密请求、解析该请求、将请求的内容映射为特定 servlet 的责任、确保特定 servlet 是加载和初始化,并最终调用 Servlet 规范定义的 servlet 代码中的特定方法。

用户应用程序一直忙于等待输入

类似于 Web 服务器总是忙于侦听传入请求,控制台应用程序gui 应用程序都总是忙于侦听用户输入。他们有时可能看起来很闲,但事实并非如此。

虽然用户应用程序不会在 CPU 上持续旋转,但它们确实维护了一个与主机操作系统一起工作的线程,以了解用户事件,例如键盘输入和鼠标移动/点击。