线程与进程
简介
本文介绍进程与线程的基本概念以及它们之间的关系和区别,不同操作系统对线程的实现方式以及 Java 线程模型的特点,上下文切换、并发与并行。
进程
存储在硬盘的静态文件(源代码),通过编译后生成二进制可执行文件,当运行这个可执行文件后,它会被加载到内存中,接着 CPU 会执行程序中的每一条指令,即运行中的程序,就被称为进程(Process)。
线程
线程(Thread)是进程内的执行单元,是 CPU 调度的基本单位。每个进程至少包含一个线程(主线程),也可以创建多个线程来执行不同的任务。
进程与线程区别
特性 | 进程 | 线程 |
---|---|---|
定义 | 运行中的程序的实例,是系统进行资源分配和调度的基本单位 | 进程中的执行单元,是 CPU 调度的基本单位 |
资源占用 | 拥有独立的内存空间、文件描述符等系统资源 | 共享所属进程的内存空间和资源 |
通信方式 | 进程间通信(IPC):管道、消息队列、共享内存、信号量、套接字等 | 线程间可直接通过共享变量通信 |
切换开销 | 切换开销大,涉及到虚拟内存、页表等切换 | 切换开销小,只需保存和恢复少量寄存器内容 |
创建销毁开销 | 创建和销毁开销大 | 创建和销毁开销小 |
并发性 | 多进程并发 | 多线程并发 |
健壮性 | 一个进程崩溃不会影响其他进程 | 一个线程崩溃会导致整个进程崩溃 |
线程实现
不同操作系统对线程的实现方式有所不同,主要可以分为三种实现模型:用户级线程模型、内核级线程模型和混合线程模型。
1. 用户级线程模型(User-Level Threads)
用户级线程是完全建立在用户空间的线程库上,系统内核不能感知用户级线程的存在。
特点:
线程管理在用户空间完成,不需要内核支持
线程切换无需切换到内核态,开销小
可以实现特定的调度算法
可以支持大规模的线程数量
缺点:
一个阻塞型系统调用会导致整个进程阻塞
无法利用多处理器的并行能力
2. 内核级线程模型(Kernel-Level Threads)
内核级线程(KLT)是由操作系统内核支持的线程,系统内核负责线程的创建、调度和管理。
特点:
内核直接支持和管理线程
一个线程阻塞不会导致整个进程阻塞
可以充分利用多处理器的并行能力
缺点:
线程创建和切换的开销较大
线程数量受系统资源限制
3. 混合线程模型(Hybrid Threading Model)
混合模型结合了用户级线程和内核级线程的优点,实现了多对多的线程映射关系。
特点:
结合了用户级和内核级线程的优点
可以同时支持大量线程和多处理器并行
灵活性更高
实现方式:
多对一模型(M:1):多个用户线程映射到一个内核线程,即上述的用户级线程模型
一对一模型(1:1):每个用户线程映射到一个内核线程,即上述的内核级线程模型
多对多模型(M:N):M 个用户线程映射到 N 个内核线程,综合了前两种模型的优点
不同编程语言对线程的实现采用了不同的模型:
语言/平台 | 线程模型 | 特点 |
---|---|---|
Java (传统线程) | 1:1 模型 | 每个 Java 线程映射到一个操作系统线程,重量级 |
Java (Virtual Threads, Java 21+) | M:N 模型 | 大量虚拟线程映射到少量操作系统线程,轻量级 |
Go (goroutine) | M:N 模型 | 轻量级协程,可以创建数十万个 |
Python (CPython) | 用户级线程 | 受全局解释器锁 (GIL) 限制,无法真正并行 |
Node.js | 事件循环 + 线程池 | 主线程处理事件循环,线程池处理阻塞操作 |
C/C++ (POSIX threads) | 1:1 模型 | 直接映射到操作系统线程 |
Java 线程模型
Java 采用基于操作系统原生线程的一对一线程模型,具有以下特点:
Java 线程与操作系统线程直接映射:每个 Java 线程对应一个操作系统线程
线程调度依赖操作系统:线程的调度由操作系统完成,JVM 不负责线程调度
线程优先级映射:Java 线程的优先级会映射到操作系统线程的优先级,但具体映射关系依赖于操作系统
轻量级进程(LWP):在某些系统上,Java 线程实际上是基于轻量级进程实现的
注意
与 Go 语言的协程(goroutine)不同,Java 的线程是重量级资源,创建和销毁成本较高。Java 21 引入的虚拟线程是对此问题的解决方案。
上下文切换
上下文切换(Context Switch)是指 CPU 从一个线程或进程切换到另一个线程或进程的过程。在这个过程中,需要保存当前线程的状态(上下文),并加载另一个线程的状态。
什么是线程上下文
线程上下文包括:
寄存器状态:包括程序计数器(PC)、堆栈指针等
线程特有数据:如在 Java 中的 ThreadLocal 变量
内存映射信息:线程当前访问的内存区域
资源句柄:线程打开的文件描述符等
调度信息:优先级、状态等
上下文切换的过程
一次完整的上下文切换主要包括以下步骤:
保存当前线程上下文:将当前线程的寄存器值、程序计数器等保存到内存中
选择下一个要运行的线程:调度器根据调度算法选择下一个线程
恢复下一个线程的上下文:从内存中加载目标线程的寄存器值、程序计数器等
切换到新线程执行:CPU 开始执行新线程的指令
并发与并行
并发和并行是多线程编程中两个核心概念,二者有本质区别,理解这些差异有助于设计高效的多线程应用。
并发(Concurrency)
并发是指在同一时间段内,多个任务交替执行。从宏观上看,这些任务似乎是同时进行的,但从微观上看,在单核处理器上,任意时刻只有一个任务在执行。
特点:
多个任务交替执行,轮流使用 CPU
适用于 I/O 密集型任务
通过时间片轮转实现
目标是提高资源利用率和响应性
并行(Parallelism)
并行是指在同一时刻,多个任务真正同时执行。这要求系统具有多核处理器或多处理器架构,每个核心同时处理不同的任务。
特点:
多个任务同时执行
需要多核/多处理器环境
适用于计算密集型任务
目标是提高吞吐量和计算速度
并发与并行的比较
下表详细比较了并发和并行的主要特性:
特性 | 并发 | 并行 |
---|---|---|
定义 | 多个任务交替执行 | 多个任务同时执行 |
执行方式 | 时间片轮转 | 真正的同时执行 |
硬件要求 | 单核即可 | 需要多核/多处理器 |
资源竞争 | 存在竞争,需要同步 | 各自独立执行,潜在竞争少 |
复杂度 | 通常更复杂(需处理竞态条件) | 相对简单(数据隔离时) |
适用场景 | I/O 密集型、交互型应用 | 计算密集型、数据处理 |
设计重点 | 资源共享和同步 | 任务分解和负载均衡 |
可扩展性 | 受单核性能限制 | 可随核心数增加而线性扩展 |