执行者框架
你说:
旋转其他用户线程
希望您没有直接寻址 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 technology 和WebSockets。
关于:“如果我们说它是在后台运行的网络服务器而不是实际的网络应用程序”,网络服务器总是运行一个额外的线程来监听传入的请求。
➥ 这可能是您困惑的根源:Web 服务器总是很忙,忙于侦听传入的连接,无论是否执行您的 Java servlet 代码。
- Web 服务器有一个线程专用于与主机操作系统协作以侦听网络上的传入连接。
- Web 服务器根据需要启动其他线程,以通过制定和发送响应来响应请求。
关于:“即使没有人与之交互,Web 应用程序如何无限期地运行”,您忘记了主机操作系统正在与 Web 容器交互,无论用户是否与您的 Web 应用程序交互。 Web 容器维护一个监听传入连接的线程。该线程处于阻塞状态,等待主机操作系统的网络堆栈通知传入请求。 (我在这里的描述是概括和简化的——我不是网络专家——但足以说明这里的观点。)
当请求通过网络进入时,主机操作系统会通知 Web 容器。此通知解除对侦听线程的阻塞。侦听线程将请求分派给新线程,从而执行 Java servlet 的代码。同时,Web 容器的请求侦听线程返回被阻塞状态,以等待来自主机操作系统的网络堆栈的另一个关于另一个传入请求的通知。
阻塞的侦听线程解释/启用网络服务器连续无限期运行。相比之下,您的 Web 应用程序会突然运行,只是为了响应请求。
您的问题归功于 Java Servlet 技术的天才和成功。 Java Servlet 的真正目的是抽象出所有这些关于监听网络活动、将数据包转换为文本以解密请求、解析请求、将请求的内容映射为特定 servlet 的责任的细节,确保特定的 servlet 是加载和初始化,并最终调用 Servlet 规范定义的 servlet 代码中的特定方法。
用户应用一直忙于等待输入
类似于 Web 服务器总是忙于侦听传入的请求,console apps 和 gui apps 都一直忙于侦听用户输入。他们有时可能看起来很闲,但实际上并非如此。
虽然用户应用程序不会在 CPU 上连续旋转,但它们会维护一个与主机操作系统一起工作的线程,以通知用户事件,例如键盘输入和鼠标移动/点击。