【问题标题】:How does a Java application keeps on running even when main thread has finished execution?即使主线程执行完毕,Java 应用程序如何继续运行?
【发布时间】:2021-03-28 11:02:19
【问题描述】:

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

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

主线程启动的用户线程会在某个时间点完成执行并终止。这将导致主线程最终终止,应用程序将停止运行。但是,当我们看到如此多的应用程序一旦开始继续运行,如果我们进行线程转储,我们可以在最底部看到主线程。这意味着主线程没有终止,这意味着启动的用户线程没有终止。

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

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

【问题讨论】:

  • 你有例子可以证明这一点吗?
  • 还有:启动线程什么的时候没有父子关系。当您启动一个线程时,它是您进程中的一个独立线程。
  • 您指的是控制台应用程序、gui 应用程序还是 Web 应用程序?
  • 嗨@akuzminykh,任何应用程序都可以是示例、Web 应用程序、服务器(如 tomcat)、基于桌面的应用程序、企业级银行应用程序。
  • @BasilBourque 我们可以举任何应用程序的例子,无论是企业级应用程序、服务器、Web 应用程序、文本编辑器,任何东西都可以是继续运行但不使用无限循环的示例保持线程活着。

标签: java multithreading thread-safety java-threads


【解决方案1】:

执行者框架

你说:

旋转其他用户线程

希望您没有直接寻址 Thread 对象。从 Java 5 开始,对于大多数目的,我们可以使用 Executors 框架来管理后台线程上的工作。请参阅 Oracle 的 tutorial

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

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

你说:

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

不正确。

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

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

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();
    }
}

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

运行时,请注意使用已提交的任务启动执行器服务如何在此处产生两个 ID 为 14 和 15 的线程。然后后台任务结束并关闭executor服务后,ID为14的线程就消失了。注意主线程是如何没有结束,继续工作——这与你在问题中的陈述相矛盾。

/Library/Java/JavaVirtualMachines/jdk-16.jdk/Contents/Home/bin/java -javaagent:/Users/basilbourque/Library/Application Support/JetBrains/Toolbox/apps/IDEA-U/ch-0/203.5981.155/IntelliJ IDEA 2020.3 EAP.app/Contents/lib/idea_rt.jar=49389:/Users/basilbourque/Library/Application Support/JetBrains/Toolbox/apps/IDEA-U/ch-0/203.5981.155/IntelliJ IDEA 2020.3 EAP.app/Contents/bin -Dfile.encoding=UTF-8 -classpath /Users/basilbourque/IdeaProjects/Loom/target/classes work.basil.example.Threading
---------------|  main thread  |------------------------------------
Bonjour. 2020-12-18T07:30:21.107455Z

Thread: 1 | main
Thread: 2 | Reference Handler
Thread: 3 | Finalizer
Thread: 4 | Signal Dispatcher
Thread: 11 | Common-Cleaner
Thread: 12 | Monitor Ctrl-Break
Thread: 13 | Notification Thread

---------------|  background thread  |------------------------------------
DEBUG - Starting background thread. 2020-12-18T07:30:21.268025Z
DEBUG - Sleeping background thread. 2020-12-18T07:30:21.268225Z

Thread: 1 | main
Thread: 2 | Reference Handler
Thread: 3 | Finalizer
Thread: 4 | Signal Dispatcher
Thread: 11 | Common-Cleaner
Thread: 12 | Monitor Ctrl-Break
Thread: 13 | Notification Thread
Thread: 14 | pool-1-thread-1
Thread: 15 | Attach Listener

DEBUG - Ending background thread. 2020-12-18T07:30:23.275729Z
---------------|  main thread  |------------------------------------

Thread: 1 | main
Thread: 2 | Reference Handler
Thread: 3 | Finalizer
Thread: 4 | Signal Dispatcher
Thread: 11 | Common-Cleaner
Thread: 12 | Monitor Ctrl-Break
Thread: 13 | Notification Thread
Thread: 15 | Attach Listener

DEBUG - Main thread ending. 2020-12-18T07:30:26.271499Z

Process finished with exit code 0

为了好玩,请尝试该代码,但颠倒持续时间。使用 5 秒而不是 2 秒来查看后台线程的寿命比主线程长。

网络服务器一直忙于监听

你在a comment说:

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

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

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

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

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

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

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

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

阻塞的侦听线程解释/启用网络服务器连续无限期运行。相比之下,您的 Web 应用程序会突然运行,只是为了响应请求。

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

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

类似于 Web 服务器总是忙于侦听传入的请求,console appsgui apps 都一直忙于侦听用户输入。他们有时可能看起来很闲,但实际上并非如此。

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

【讨论】:

  • 一个很好的答案,很好的解释!一年前,我曾经看过你关于现代日期时间的答案,我发现它们制作精良,所以我请了几天假,和我一样多地研究了现代日期时间 API可以。除了您对日期时间的回答外,我发现 Ole 的回答也很有帮助。一个月左右,我看到你发布了多线程和并发问题的答案,看来我又要请几天假来研究 Java 的现代并发 API。
  • @ArvindKumarAvinash 很高兴听到我能提供帮助。至于这个月,我对新的Project Loom 技术很感兴趣,它带有虚拟线程,很快或半很快就会出现在 Java 中。我发现在 Stack Overflow 上提问和回答问题是提高我学习能力的好方法。确实,起初我认为 Project Loom 解决方案可能适用于这个问题,但一旦我进入它,结果就变得无关紧要了。
  • 嘿@BasilBourque 非常感谢您付出的大量努力和细节。你的回复真的很有帮助。我感谢所有的解释。我可以使用所有的细节。我必须说一个令人愉快的答案/评论。我将运行您发布的代码,如果我有任何进一步的疑问,我会在此处发布。谢谢:) :)
猜你喜欢
  • 1970-01-01
  • 2013-04-27
  • 2013-03-05
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多