[英]What happens at runtime when we have multiple Java threads?
我试图了解当您有多个线程对象并在它们上调用 start 时会发生什么。
为此,我编写了以下代码:
public class Testing {
public static void main(String[] args) {
for(int i =0; i <100; i++){
(new TestThread()).start();
}
}
}
class TestThread extends Thread {
@Override
public void run() {
System.out.println("Thread instance: " + Thread.currentThread().getName());
}
}
所以我得到的 output 涉及 Thread-x,其中 x 是从 0 到 99,但它们的顺序与自然顺序不同(即 0、1、2、3、...)。 我预料到了这一点,因为我读到我们无法控制这些线程运行时会发生什么,但我想要求澄清运行时究竟发生了什么。
是不是主线程经历了创建这些线程的所有 100 次 for 循环迭代,然后 JVM 稍后任意决定这些 Thread 对象中的每一个何时启动?
谢谢。
我想要求澄清运行时到底发生了什么。
实际发生的是,当您调用start()
时, JVM通常1对操作系统进行系统调用以执行以下操作:
为新线程堆栈分配 memory 段。 (通常分配两个段:一个段用于线程堆栈,第二个只读段用于检测堆栈溢出。)
创建一个新的本地线程2 。
创建本机线程时,它必须等待(与所有其他当前准备运行的线程一起)操作系统的线程调度程序将其调度到物理内核。 一般来说,操作系统的线程调度器尊重优先级,但相同优先级的线程之间的调度通常不是“公平的”; 即不保证“先到先得”。
因此,在某些时候,操作系统将安排新的本机线程运行。 发生这种情况时,线程将执行一些获取Runnable
引用并调用其run()
方法的本机代码。 相同的代码将处理来自run()
方法的任何未捕获的异常。
确切的细节将是 JVM 特定和操作系统特定的,您不需要知道他们。
是不是主线程经历了创建这些线程的所有 100 次 for 循环迭代,然后 JVM 稍后任意决定这些 Thread 对象中的每一个何时启动?
不必要。 它可能会,也可能不会。
实际发生的情况将取决于操作系统的本机代码调度程序如何处理新创建的本机线程。 这将取决于各种难以预测的因素。 例如,其他线程和其他应用程序的行为等等。
基本上,不能保证3子线程将以任何特定顺序开始执行,或者主线程将或不会在任何子线程启动之前完成循环。
1 - 这是 JVM 的典型情况,它提供 Java 线程和本机线程之间的 1 对 1 映射。 这是当前大多数 JVM 的行为方式,但它并不是 model 的唯一实现。
2 - 本机线程是操作系统支持的线程。 有关更多信息,请参阅Java 线程模型,并以本机 POSIX 线程库为例。
3 - 在某些平台和负载条件下,您可能能够观察到行为模式,但您可能会发现其他平台上的行为有所不同等。
斯蒂芬 C 的答案是正确的,并且信息丰富。 我想添加两个不同的点:执行程序和虚拟线程。
你的代码说:
新的 TestThread()).start()
在现代 Java 中,我们很少需要直接处理Thread
class。 相反,我们使用添加到 Java 5 的 Executors 框架。 ExecutorService
减轻了我们处理线程的负担。
将您的任务编写为Runnable
/ Callable
对象,以提交给您选择的执行器服务实现。
这是您的代码的修订版本,针对执行程序服务进行了重新调整。
请注意我是如何选择使用最多 4 个线程的后备池newFixedThreadPool
的ExecutorService
实现的。 对于像您这样的简短任务,我们可能会选择newCachedThreadPool()
作为无界线程池。 要按顺序运行您的任务,我们可以使用newSingleThreadExecutor()
。
小注:我从获取线程的名称更改为获取它的 ID 号。 线程并不总是有名称。
package work.basil.demo.threads;
import java.time.Instant;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
public class App2
{
public static void main ( String[] args )
{
System.out.println( "INFO - The main method of our demo is starting. " + Instant.now() );
Runnable task = ( ) -> System.out.println( "Thread instance: " + Thread.currentThread().getId() + " | " + Instant.now() );
ExecutorService executorService = Executors.newFixedThreadPool( 4 );
for ( int i = 0 ; i < 100 ; i++ )
{
executorService.submit( task );
}
executorService.shutdown();
try { executorService.awaitTermination( 30 , TimeUnit.SECONDS ); } catch ( InterruptedException e ) { e.printStackTrace(); }
System.out.println( "INFO - The main method of our demo is ending. " + Instant.now() );
}
}
跑的时候。
INFO - The main method of our demo is starting. 2021-06-06T06:57:31.423358Z
Thread instance: 17 | 2021-06-06T06:57:31.437343Z
Thread instance: 16 | 2021-06-06T06:57:31.435732Z
Thread instance: 14 | 2021-06-06T06:57:31.432704Z
Thread instance: 15 | 2021-06-06T06:57:31.433343Z
Thread instance: 14 | 2021-06-06T06:57:31.445160Z
Thread instance: 16 | 2021-06-06T06:57:31.445206Z
…
Thread instance: 16 | 2021-06-06T06:57:31.449733Z
Thread instance: 16 | 2021-06-06T06:57:31.449776Z
Thread instance: 14 | 2021-06-06T06:57:31.448232Z
Thread instance: 14 | 2021-06-06T06:57:31.449871Z
Thread instance: 14 | 2021-06-06T06:57:31.449923Z
Thread instance: 15 | 2021-06-06T06:57:31.447659Z
Thread instance: 14 | 2021-06-06T06:57:31.449964Z
Thread instance: 16 | 2021-06-06T06:57:31.449816Z
Thread instance: 17 | 2021-06-06T06:57:31.449404Z
INFO - The main method of our demo is ending. 2021-06-06T06:57:31.450433Z
您问:
JVM 稍后任意决定这些线程对象中的每一个何时启动
当我们调用ExecutorService#submit
时,执行器服务负责实际运行该任务,顾名思义。 执行器服务会尝试尽快运行任务,具体取决于可用的线程。 您的任务实际开始工作需要多长时间会有所不同,具体取决于 JVM 的 state、主机操作系统在调度线程以在 CPU 内核上执行时使用的算法,以及主机硬件的负担程度。
您可以通过捕获submit
方法返回的Future
object 来跟踪执行的进度,并询问任务是完成还是取消。
如果使用ScheduledExecutorService
,您可以选择在运行任务之前指定延迟。
了解 Stephen C 在该答案中讨论的本机线程相对“重”/“昂贵”。 每个线程的创建速度相对较慢,通常从大量堆栈(通常是兆)开始,并且由于在 OS kernel 空间和userland之间切换而在 CPU 内核上切换一些执行时间时有些慢。
如果Project Loom成功,Java 将获得一种额外的线程,虚拟线程,也称为纤维。 这些轻量级虚拟线程在 JVM 内进行管理,而不直接涉及主机操作系统。
许多虚拟线程被映射为实际运行在少数本机/平台线程上。 默认情况下,虚拟线程具有较小的堆栈,可以根据需要增长和收缩(,)。 与原生线程不同,虚拟线程在阻塞时(例如等待文件系统、网络访问、数据库响应等)会自动“停放”,即。 留出以便另一个虚拟线程可以切换到本机线程上以立即执行。 这种切换将非常快,因为它可以由涉及主机操作系统的 JVM 管理。
这些特性意味着虚拟线程非常“便宜”,占用的 memory 和 CPU 都非常少。 甚至数百万个线程在传统计算机硬件上也可能是实用的。
此外,虚拟线程消除了对线程池的需要。 这意味着在使用回收线程之间没有剩余ThreadLocal
值泄漏的威胁。
虚拟线程适用于运行阻塞的代码。 这适用于 Java 中通常开发的大多数普通业务应用程序。 但是,对于真正的 CPU 密集型任务(例如视频编码),请坚持使用传统线程。
更改代码以使用虚拟线程就像更改ExecutorService
的实现一样简单。
ExecutorService executorService = Executors.newVirtualThreadExecutor() ;
有关更多信息,请参阅 Ron Pressler 和 Oracle 从事 Project Loom 的其他人最近的视频演示和采访。 如果您想了解更多信息,现在可以基于早期访问 Java 17 获得实验性构建。
声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.