Java线程创建漫谈—冰川之下

发布时间:2026/5/24 15:44:03

Java线程创建漫谈—冰川之下 网上已有很多文章讲过Java线程创建方式,本文尝试用不一样的视角去谈。这篇万字长文将由浅入深,从一个略有争议的话题开始,它将是串联起其他各类问题的线索,对于面试、工作、筑基,或有些许助力。一、线程创建方式1.1、创建方式是多种,还是1种?听说Java创建线程有4种常见方式;也有些辟谣说其实只有1种,情况究竟如何?常见4种包括:① 继承Thread类② 实现Runnable接口③ 实现Callable接口 + FutureTask包装④ 使用线程池这些可以大致归纳为:new Thread(xx),区别在于xx有丰富的方式(如图),这就是常说的创建线程方式有多种所对应的含义了。但若细究,会发现“创建线程”的表述并不准确:new Thread()是创建了线程对象,但此时也仅仅是JVM堆中的一个普通的Java对象;Thread.start()内部才真正触发了操作系统层面的线程的创建,进而触发执行具体线程任务。因此说Java创建线程的方式只有1种:Thread.start()。而常说的4种或更多方式,严格来说是定义线程任务、构造线程对象的方式。1.2、关联的各种问题那么有必要改口吗?咬文嚼字没意义,关键看是否会造成误解。如果将语境限定在Java层面,似乎不会误解;另外一般new Thread()和start()紧邻使用,即使真的误以为前者创建线程,也没啥实质影响。但是且慢,start()的一个重要影响是,它会占用一定的本地内存(Java线程栈内存的典型默认值1MB)。假如存在这种情况:大量地new Thread()却尚未start(),那么误以为new Thread()已创建线程,则可能影响对内存占用情况的判断。首先想到的是线程池OOM场景,它的阻塞队列里的任务是否属于已new Thread()但未start()的情况?毕竟线程池对使用者是黑盒,需“开盒”分析才能确认。除了线程池,还有几个问题:start()创建了OS线程,那么OS线程跟Java线程之间是什么关系?start()底层究竟做了什么?从定义子线程任务,到start()后执行任务,这个“任务”是如何传递、关联的?PS: 除开特定小节,本文提到的“线程”不包括虚拟线程。翻译问题在探讨各问题之前,对于“创建线程”的说法先排除下是否是翻译的锅。Java经典资料里,确实也直接将new Thread()称为创建线程(create a thread)——例如《Java核心技术》第10版14章:Oracle文档:但这些资料在给thread下定义时,却是按操作系统线程的概念定义的:a thread of execution, thread of control。总之,thread单词是两种含义混用:既可以指操作系统线程,又可以指Thread实例对象;那么中文语境里“线程”同样混用含义也不是很意外了。二、Java线程 vs OS线程在分析线程池OOM是否大量new Thread()而未start()之前,先理解一下Java线程是怎样的一种存在。在接触Java线程的早期,我模糊地以为Java自己实现了某种线程、并且能某种程度参与线程调度(sleep/park/unpark),后来才发现并非如此。2.1、Java线程和OS线程,你俩什么关系?《深入理解Java虚拟机》书中对主流JVM实现的线程的描述:“每一个Java线程都是直接映射到一个操作系统原生线程来实现的”。但这样表述可能产生一种印象:Java线程跟OS线程,像是并列的两个线程实体。更直白具体的描述是这样的:每个Java线程(Thread实例)都是一个OS原生线程的封装。它额外维护了一些Java作为上层应用所需的一些管理信息,例如Java层的线程状态、Monitor锁、ThreadLocal存储等等。光语言描述可能抽象,下图展示了Java的Thread对象是如何层层封装了底层的OS线程,各层对象之间均通过指针或字段进行关联。不难发现,从Java Thread到OS线程(pthread),Java套了3层壳。为什么套这么多层?通俗地说:OSThread层是为了兑现“编译一次、到处运行”的承诺,屏蔽操作系统差异;JavaThread层是JVM真正管理、维护线程的对象;Thread层是为了让程序员能傻瓜式地操作线程而无需操心底层复杂细节;对象功能层次作用Thread(Java)API层为程序员提供start/sleep/interrupt等API,屏蔽了线程底层的复杂细节JavaThread(C++)JVM逻辑层管理Java的执行上下文(栈帧、安全点、异常)、维护线程状态、协同GCOSThread(C++)平台适配层屏蔽操作系统差异,如果没有它,JavaThread里就得对不同系统做if判断了至此,在Java语境中将new Thread()称为创建线程,从以下角度看有合理性:1、抽象封装:new Thread()和start()是Java提供的线程API,屏蔽了OS层的细节(如pthread_create调用、栈内存分配、内核结构体);2、生命周期模型差异:Java给Thread定义了6种状态,跟OS通用的5态模型并非一一映射,且自定义了状态流转机制。总之在Java的语境中,“线程”可理解为Java层封装后的线程:不完全等同于OS线程,而是更抽象、更上层的存在。那么Java的线程状态相对于OS层的有何区别,只是细化了阻塞态、合并了就绪/运行态吗?另外JDK19起推出的虚拟线程又是怎么回事?2.2、虚拟线程Java传统线程面对IO密集型任务,配置线程数某种程度上存在一根筋两头堵的局面:如果配的少,线程大部分时候在等待IO完成,CPU闲置浪费;如果配的多,线程频繁切换、上下文切换成本较大,对CPU也是无谓浪费。实践中可根据经验配置合适值以获得最大吞吐量,但CPU切换成本仍不容忽视。总之传统模式下的痛点是,Java传统线程(平台线程)本质上作为OS线程的封装,同时还承载了线程任务,想启动/切换任务就得创建/切换OS线程。而JDK19起推出的虚拟线程的精髓,是“解耦”、“复用”:用虚拟线程承接任务,实现平台线程与任务解耦,一个平台线程可同时承接大量任务。执行任务时才将虚拟线程“挂载”到平台线程去执行,这样切换任务只需低成本地切换虚拟线程即可,免去了切换OS线程的成本。IO密集型场景,可以创建大量虚拟线程以提高CPU利用率,但同时平台线程只需维持少量数目即可。线程的成本先简单回顾下,常说线程创建/切换的成本高,成本大概在哪——CPU成本内存成本创建切换内核态等系统调用占CPU时间1~几MB的内存切换CPU时间占用(切换内核态、保存/恢复上下文)未能利用局部性原理引起的cache miss(相关数据需消耗CPU周期重新读取)\我们最关心的线程切换成本,主要是直接和间接对CPU时间的占用。一个仅关于效率层面的类比:高铁350km/h如果一站不停直达,那得有多快;因停靠多站点而间歇性刹车、上下客、启动,又会慢多少?虚拟线程的成本创建:虚拟线程只是Java堆中的对象,创建成本极低:占用内存远小于OS线程,不涉及系统调用相关的CPU成本;切换:虚拟线程的上下文的保存/恢复仅发生在JVM内;平台线程(载体线程)并未切换,因此免去了昂贵的 OS线程上下文切换、对CPU缓存的影响更小。那么虚拟线程的上下文切换是如何实现的?虚拟线程的上下文(栈帧、局部变量、执行位置)被封装为Continuation对象,位于JVM堆中。当虚拟线程因执行阻塞操作而主动让出(yield)载体线程,上下文数据被保存到堆内存的Continuation对象中,并释放载体线程;当虚拟线程重新被调度到载体线程上执行,堆内存Continuation里的上下文数据将拷贝回载体线程的栈中,从断点处继续执行。当然,虚拟线程也有局限性,后面再谈。2.3、线程的调度书上说Java线程(平台线程)的调度完全由OS负责。那这些阻塞/唤醒方法(sleep/park/unpark/notify)算什么?虚拟线程的调度跟OS调度有关

相关新闻