序言
看网上说阿里二面问到了一个看似最简单且没有标准答案的一个问题,所有学习编程都是从打印hello World开始的,那运行打印启动了多少个线程?
启动了多少线程?
在运行一个简单的 “Hello World” 程序时,启动的线程数量取决于编程语言、运行时环境和操作系统。
就编程语言而言,C语言、C++、Java、Python、Go、JavaScript都是不同的,且最少有一个线程。如果不考虑编程语言的话,就Java语言,有多少个线程也不是确定的。需要看JDK的版本,不同JDK版本启动的线程数量也可能是不同的。
比如下面这个简单的Java程序,怎么看它运行了多少个线程呢?
public class Main {
public static void main(String[] args) {
System.out.println("Hello World");
}
}
假设当前电脑JDK版本是Java 8,
strace命令
方法有很多,如果你是linux环境,使用strace
·(system trace是 Linux 系统中一个功能强大的调试、分析、诊断工具,可以对系统调用和信号传递的跟踪结果进行分析)命令进行分析,
- -c:统计每个系统调用的时间、次数和错误,并在程序退出时报告摘要。
- -D:将跟踪进程作为分离的孙进程运行,减少strace的可见效果。
- -d:输出strace关于标准错误的调试信息。
- -f:跟踪由fork调用所产生的子进程。
- -t:在输出中的每一行前加上时间信息。
- -v:输出所有的系统调用,包括环境变量和状态信息。
- -o:将跟踪结果输出到文件中。
下面的h1就是分析的结果文件,后缀就是对应的线程编号,即下面有很多个线程,less h1.11767
可以查看对应的线程内容
上面代表的是linux操作系统的系统调用日志。
JProfiler工具
JProfiler(Java Profiler)是IDEA自带的Java性能分析工具。左边有几个线程ID,比如线程ID=13571的线程,JavaThread::run() -> CompileBroker::invoke_compiler_on_method(CompileTask*),它是JVM中JIT即时编译器线程,用于在程序运行时将代码转换为机器码。
下面线程id = 17155的线程,JavaThread::run() -> Compile::Compile(ciEnv*, C2Compiler*, ciMethod*, int, bool, bool, bool),它是进行编译的线程,入参有编译器接口环境指针,C2编译器指针,要编译的方法等参数,
destroyjavavm线程,它是用于销毁JVM实例的线程,
Monitor Ctrl-Break监控线程,用于监控控制台中断信号,
还有Finalizer 线程是一个由 JVM 自动创建的守护线程(Daemon Thread),负责执行对象的 finalize() 方法。它是 Java 垃圾回收(Garbage Collection, GC)机制的一部分。
Thread.getAllStackTraces方法
如下图,该方法获取了所有活动线程的堆栈跟踪信息。
- Thread[Signal Dispatcher,9,system]=[Ljava.lang.StackTraceElement;@579bb367
- Thread[main,5,main]=[Ljava.lang.StackTraceElement;@1de0aca6,
- Thread[Monitor Ctrl-Break,5,main]=[Ljava.lang.StackTraceElement;@255316f2,
- Thread[Finalizer,8,system]=[Ljava.lang.StackTraceElement;@41906a77,
- Thread[Reference Handler,10,system]=[Ljava.lang.StackTraceElement;@4b9af9a9
其中Reference Handler线程是Java虚拟机(JVM)中的一个特殊线程,用于处理垃圾回收过程中的对象引用。在Java中,对象引用可能会被垃圾回收器回收,但是如果仍然有其他对象引用这些对象,那么这些对象就不会被回收。Reference Handler线程的作用就是处理这些被引用但不再被使用的对象,以避免内存泄漏。
Reference Handler线程的主要功能是处理四种类型的引用:
- SoftReference:软引用,只有在内存不足时才会被回收。
- WeakReference:弱引用,在下一次垃圾回收时会被回收。
- PhantomReference:虚引用,用于在对象被回收之前执行一些操作。
- FinalReference:最终引用,用于处理具有finalize方法的对象。
进程、线程和协程的区别
- 进程:系统资源分配的最小单位,每个进程拥有独立的内存空间、文件描述符等资源,进程间完全隔离。
- 线程:运行在进程内部,共享进程的内存空间和文件句柄等资源,但每个线程有自己的栈和局部变量,线程间通过共享资源直接通信。
- 协程:在单线程内通过用户态调度实现并发,不涉及系统资源分配,仅在用户态切换执行流,因此没有独立的内存。
进程(Process)
进程是程序在计算机上运行的一个实例,那一个应用是否是一个进程呢?大多数情况如此,但有些应用中可能包含多个进程,比如VS Code就包含主进程、渲染进程和插件进程。
线程(Thread)
线程是操作系统内核调度的基本单位,线程的创建、销毁、多线程之间的切换都需要操作系统的内核完成。
协程(Coroutine)
而协程则是利用同一个调度线程去实现协程之间的切换,从而避免了CPU在内核态和用户态之间的来回切换,所以协程的效率比线程要高,比如Go语言天然支持协程,它在协程的帮助下可以轻松实现百万并发。
那为什么Java一开始就没有用协程的设计呢?原因是由于Java诞生于1995年,多线程模型已经成熟,且被各种操作系统广泛支持。而协程还没有标准化的支持,在不同操作系统上实现方式差距很大,兼容性也不好,所以Java就选择了更加成熟的线程模型。
之后Java在多线程的路上越走越深,为了解决频繁创建和销毁线程导致的性能开销,衍生出了线程池技术。
但是始终无法解决下面两个问题:
- 1、线程太多,会导致CPU频繁上下文切换,性能开销大;
- 2、线程太少,系统吞吐量又不足;
所以衍生出了多线程参数调优:
- 1、识别适合并行执行的任务;
- 2、保证线程安全;
- 3、避免死锁和竞态条件;
- 4、合理分配核心线程数;
- 5、优化线程间通信;
- 6、持续监控和优化;
虚拟线程(Virtual Threads)
JVM)在处理多线程应用时,会使用线程上下文来执行各个线程的任务。然而,在某些情况下,线程上下文可能会成为性能瓶颈。为了解决这个问题,JDK19引入了一项新特性——虚拟线程(Virtual Threads,简称VTs)。虚拟线程是一种在JVM内部实现的并行执行机制,可以提高多线程应用的性能。
与传统的线程相比,虚拟线程具有以下特点:
-
无锁竞争:虚拟线程在执行过程中不需要进行锁竞争,这意味着它可以显著降低线程切换的开销,从而提高程序的运行效率。
-
无需线程上下文:虚拟线程在执行过程中不需要线程上下文,这意味着它可以更好地利用处理器资源,提高程序的并行执行能力。
-
更低的内存开销:虚拟线程的实现方式相对于线程更为轻量级,因此它在内存占用方面具有更小的开销。
看网上的文章是使用@RunOnVirtualThread注解通过AOP去使用虚拟线程,目前开发使用的场景还没有使用过虚拟线程,也不清楚实际的效率提升是多少,需要各位工程师在实践上去测试。