【并发任务调度】01. 线程基础
在当今高度并发的应用环境中,线程管理对于构建高效、响应迅速的系统至关重要。Java 语言的线程模型不断演进,从最初的用户线程到现在的内核线程映射,并在 Java 21 中引入了虚拟线程,旨在提升并发处理能力和资源利用率。本文将概述 Java 线程的不同实现方式,探讨虚拟线程如何通过轻量级设计优化阻塞任务的处理,并对比操作系统与 Java 层面的线程状态模型,为开发者提供实用的并发编程指导。
1 线程模型
1.1 线程映射方式
线程其实是操作系统层面的实体,任何语言实现线程主要有三种方式:
- 内核线程实现(1:1实现):每个Java线程直接映射到一个操作系统线程,由操作系统负责线程的调度和管理。这种方式的好处在于调度简单且可靠,但创建和销毁线程的成本较高。
- 用户线程实现(1:N实现):完全在用户空间中实现,多个Java线程共享一个操作系统线程,线程的调度和管理不需要操作系统直接支持。虽然这种实现方式降低了系统调用开销,但由于缺乏内核支持,处理阻塞等问题变得复杂。
- 混合实现(N:M实现):结合内核线程和用户线程的优点,即多个用户线程映射到较少的操作系统线程上。这种方式可以支持大量的并发用户线程,同时利用操作系统提供的资源管理和调度功能。
1.2 Java传统线程模型
在Java发展的早期阶段,线程的实现依赖于用户线程实现。但从JDK 1.3开始,主流的Java虚拟机转向了基于操作系统内核线程实现(1:1实现)。这意味着每一个Java线程直接映射为一个操作系统级别的线程,从而简化了线程管理并提高了性能。
1.3 Java虚拟线程模型
在Java 21中,为了提高并发编程的效率和资源利用率,JDK引入了虚拟线程(Virtual Threads)的概念,并引入了平台线程(Platform Threads)来明确区分传统的Java线程。具体来说:
- 平台线程(Platform Thread):每个
java.lang.Thread
类的实例都是一个平台线程,它是Java对操作系统线程的包装,与操作系统线程是一对一(1:1)的映射。 - 虚拟线程(Virtual Thread):一种轻量级的、由JVM管理的线程,对应的实例类型为
java.lang.VirtualThread
类。 - 载体线程(Carrier Thread):指真正负责执行虚拟线程中任务的平台线程。一个虚拟线程装载到一个平台线程之后,这个平台线程就成为该虚拟线程的载体线程。
通过这些概念,Java 21形成了一个N:M实现的线程模型,即多个虚拟线程可以共享一个操作系统线程。
虚拟线程旨在解决传统线程模型中的一些限制,提供了更高效的并发处理能力,其特点包括:
- 轻量化:资源消耗低,轻量级资源,适合大量并发任务。
- 适合阻塞任务:在执行阻塞操作时不会占用CPU资源,提高整体系统响应性和吞吐量。
- 用完即抛:不需要池化管理,减少了资源管理和维护的开销。
- 不适合CPU密集计算:虚拟线程适合处理大量并发的阻塞任务,而不是单一的高计算强度任务。
虚拟线程可以通过Thread.ofVirtual()
方法创建,如下示例所示:
public class VTDemo {
public static void main(String[] args) throws InterruptedException {
// 平台线程
Thread.ofPlatform().start(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread());
}
});
// 虚拟线程
Thread vt = Thread.ofVirtual().start(new Runnable() {
@Override
public void run() {
System.out.println(Thread.currentThread());
}
});
// 等待虚拟线程打印完毕再退出主程序
vt.join();
}
}
输出结果可能会显示如下:
Thread[#22,Thread-0,5,main]
VirtualThread[#23]/runnable@ForkJoinPool-1-worker-1
2 线程生命周期
2.1 操作系统层面
操作系统层面的线程生命周期基本上可以用下图这个“五态模型”来描述。
- 【初始状态】仅是在语言层面创建了线程对象,还未与操作系统线程关联
- 【就绪状态】(可运行状态)指该线程已经被创建(与操作系统线程关联),可以由 CPU 调度执行
- 【运行状态】指获取了 CPU 时间片运行中的状态
- 当 CPU 时间片用完,会从【运行状态】转换至【可运行状态】,会导致线程的上下文切换
- 【阻塞状态】
- 如果调用了阻塞 API,如 BIO 读写文件,这时该线程实际不会用到 CPU,会导致线程上下文切换,进入【阻塞状态】
- 等 BIO 操作完毕,会由操作系统唤醒阻塞的线程,转换至【可运行状态】
- 与【可运行状态】的区别是,对【阻塞状态】的线程来说只要它们一直不唤醒,调度器就一直不会考虑调度它们
- 【终止状态】表示线程已经执行完毕,生命周期已经结束,不会再转换为其它状态
2.2 Java层面
Java 中线程的状态分为 6 种:
- 【初始(NEW)】:新创建了一个线程对象,但还没有调用 start()方法。
- 【运行(RUNNABLE)】:线程对象创建后,其他线程(比如 main 线程)调用了该对象的 start()方法。
- 【阻塞(BLOCKED)】:表示线程阻塞于锁。
- 【等待(WAITING)】:进入该状态的线程需要等待其他线程做出一些特定动作 (通知或中断)。
- 【超时等待(TIMED_WAITING)】:该状态不同于 WAITING,它可以在指定的时 间后自行返回。
- 【终止(TERMINATED)】:表示该线程已经执行完毕。
在操作系统层面,Java 线程中的 BLOCKED、WAITING、TIMED_WAITING 是一种状态,即前面我们提到的阻塞状态,而操作系统层面的就绪和运行中两种状态JVM 层面并不关心,因为 JVM 把线程调度交给操作系统处理了,所以笼统的称为“运行”。
3 结语
本文介绍了线程模型的不同实现方式及其在Java中的发展。从早期的用户线程到Java 21引入的虚拟线程,Java不断优化其并发处理能力。虚拟线程作为轻量级的解决方案,能够有效提升系统对于大量并发阻塞任务的处理效率。理解Java线程的不同状态及其在操作系统层面的表现,可以帮助开发者更好地设计高效、可靠的多线程应用程序。