进程、线程、并发与并行 | JavaSE
进程、线程、并发与并行
一、学习目标
完成本章后,你应该能够:
- 准确解释进程(Process)、线程(Thread)、多线程(Multithreading)的基本概念。
- 说明一个 Java 程序中的进程与线程之间是什么关系。
- 区分单线程程序与多线程程序。
- 准确区分并发(Concurrency)与并行(Parallelism),避免把二者混为一谈。
- 理解 CPU、操作系统、JVM 与线程调度之间的大致关系。
- 判断一个业务场景是否适合使用多线程。
- 理解多线程能够解决什么问题,同时知道多线程并不意味着程序一定更快。
- 建立后续学习
Thread、Runnable、线程安全、锁和线程池所需要的整体模型。
二、核心知识
2.1 从“执行流程”理解线程
先看一个最普通的 Java 程序:
public class Main {
public static void main(String[] args) {
System.out.println("任务 A");
for (int i = 0; i < 5; i++) {
System.out.println(i);
}
System.out.println("任务 B");
}
}
程序大致按照:
main()
↓
任务 A
↓
for 循环
↓
任务 B
一路向下执行。
可以把这种:
从某个入口开始,按照一定顺序不断执行代码的过程
理解为一条执行流程(Execution Flow)。
而线程(Thread),就是程序中的一条执行流程。
Java 官方对 Thread 的描述也非常直接:
一个线程就是程序中的一条执行线程。
如果整个程序只有一条主要执行流程,那么它就是典型的:
单线程程序
如果程序中同时存在多条执行流程:
执行流程 1
执行流程 2
执行流程 3
...
就进入了:
多线程
2.2 什么是进程
进程(Process)可以先理解为:
正在运行的程序实例。
例如你同时打开:
IntelliJ IDEA
Chrome
微信
网易云音乐
操作系统看到的不是四份静态的磁盘文件,而是多个正在运行的程序实例。
这些正在运行的程序,就是进程。
例如:
Chrome.exe
IDEA
java.exe
都可能对应一个或多个正在运行的进程。
对于一个普通 Java 程序:
java Main
操作系统会启动一个 JVM 进程。
这个 JVM 进程内部再运行 Java 程序。
可以暂时建立这样的模型:
操作系统
└── JVM 进程
└── Java 程序
进程通常拥有相对独立的:
- 虚拟地址空间;
- 内存资源;
- 文件句柄;
- 系统资源;
- 运行状态。
不同进程之间默认具有较强的隔离性。
2.3 什么是线程
线程(Thread)是:
进程内部的一条执行流程。
因此:
进程
└── 线程
不是两个毫无关系的东西。
而应该理解为:
一个进程
↓
内部可以存在多个线程
例如:
JVM 进程
├── main 线程
├── 工作线程 A
├── 工作线程 B
└── JVM 自身运行所需要的其他线程
同一个进程中的线程通常共享这个进程中的大量资源,例如:
堆内存中的对象
打开的文件
网络连接
进程级资源
这也是后面“线程安全问题”产生的重要根源:
多个线程
↓
同时访问
↓
同一个共享数据
就可能发生竞争。
不过线程安全将在后续 08-06 专门讨论,本章暂时只建立概念。
2.4 进程和线程的关系
可以用一个非常形象的比喻理解。
假设:
星雨笔录开发工作室 = 一个进程
里面有:
程序员 A
程序员 B
程序员 C
分别做:
A:写前端
B:写后端
C:写测试
那么三个程序员就类似三条线程。
工作室拥有共同资源:
办公室
服务器
代码仓库
项目文档
类似于同一个进程中的线程共享进程资源。
因此:
| 对比 | 进程 | 线程 | | -------- | ------------------ | ---------------------------- | | 基本含义 | 正在运行的程序实例 | 程序中的一条执行流程 | | 关系 | 可以包含多个线程 | 属于某个进程 | | 资源隔离 | 不同进程相对独立 | 同进程线程共享大量资源 | | 创建成本 | 通常较高 | 通常低于创建完整进程 | | 通信 | 进程间通信相对复杂 | 共享内存更加直接 | | 风险 | 隔离较强 | 共享数据容易产生并发安全问题 |
可以记住一句话:
进程是程序运行的容器,线程是容器中的执行流程。
2.5 Java 程序中的 main 线程
当 JVM 启动一个普通 Java 应用程序后,会执行:
public static void main(String[] args)
运行 main() 方法的线程通常称为:
main 线程
例如:
public class Main {
public static void main(String[] args) {
System.out.println("Hello Java");
}
}
从初学者视角看:
JVM
↓
main 线程
↓
main()
↓
执行代码
在前面学习:
- 数组;
- 集合;
- Stream;
- File;
- IO;
的时候,大多数练习实际上都是:
main 线程自己从头执行到尾。
接下来学习多线程,就是准备让程序从:
只有一条执行流程
发展成:
多条执行流程共同工作
2.6 什么是多线程
多线程(Multithreading)指:
一个程序中存在多条可以并发执行的线程。
例如:
Java 程序
main线程 下载线程 日志线程
│ │ │
▼ ▼ ▼
处理界面 下载文件 写入日志
如果只有一个线程:
先下载
↓
下载完成
↓
再处理界面
↓
再写日志
那么某个耗时任务可能长期占据唯一执行流程。
使用多线程之后:
线程 A → 处理用户操作
线程 B → 下载文件
线程 C → 写日志
多个任务便可以在时间上重叠推进。
这就是多线程存在的重要意义。
2.7 什么是并发
并发(Concurrency)描述的是:
多个任务在一段重叠的时间范围内共同向前推进。
注意:
并发 ≠ 必须在同一个瞬间真的执行
假设 CPU 只有一个可执行核心。
有两个线程:
线程 A
线程 B
CPU 可以快速切换:
A 执行一会
↓
B 执行一会
↓
A 再执行
↓
B 再执行
形成:
时间 ─────────────────────>
CPU:
AAAA | BBB | AAA | BB | AAAA
宏观上看:
A 和 B 都在推进
于是构成并发。
这种现象经常被形象地描述成:
CPU 在多个任务之间快速切换,看起来好像同时执行。
但要注意:
“时间片轮转”是一种实现并发的方式,而不是并发本身的完整定义。
并发更重要的是:
多个任务的生命周期发生重叠
并且都在推进
2.8 什么是并行
并行(Parallelism)指:
多个任务在同一个时刻真正同时执行。
例如一颗多核心 CPU:
CPU Core 1 → 线程 A
CPU Core 2 → 线程 B
CPU Core 3 → 线程 C
CPU Core 4 → 线程 D
那么某个瞬间:
A、B、C、D
确实可能同时运行。
这就是并行。
2.9 并发与并行的区别
这是多线程学习中必须彻底理解的一组概念。
| 对比 | 并发 Concurrency | 并行 Parallelism | | --------------------- | ------------------------------ | ------------------------------- | | 核心含义 | 多个任务在重叠时间段内共同推进 | 多个任务在同一时刻真正执行 | | 是否要求多个 CPU 核心 | 不要求 | 通常需要多个执行资源 | | 单核 CPU | 可以并发 | 通常无法实现真正的 CPU 并行执行 | | 多核 CPU | 可以并发 | 也可以并行 | | 关注重点 | 多任务组织与协调 | 同时计算 |
可以用餐厅理解。
只有一个厨师:
切菜 2 分钟
炒菜 1 分钟
煮汤等待时去切菜
再回来处理汤
他在多个任务之间来回切换。
属于:
并发
如果现在有:
厨师 A → 炒菜
厨师 B → 煮汤
厨师 C → 切菜
同一时刻真的都在工作。
属于:
并行
2.10 并发与并行可以同时存在
现代计算机通常是多核心 CPU。
假设存在:
10 个线程
4 个 CPU 核心
某个瞬间最多只有其中若干线程真正占据 CPU 核心:
Core 1 → T1
Core 2 → T3
Core 3 → T7
Core 4 → T9
这些线程:
T1、T3、T7、T9
之间可能是并行执行。
但全部:
T1 ~ T10
又可能通过调度不断切换。
因此整个系统经常同时具有:
并发
+
并行
而不是“只能二选一”。
2.11 多线程为什么有价值
多线程的价值并不是简单一句:
“可以让程序更快。”
它主要解决几类问题。
场景一:提高程序响应能力
例如桌面程序正在下载一个 5 GB 文件。
如果下载和界面处理都由同一个线程承担:
开始下载
↓
长时间阻塞
↓
界面无法及时处理用户操作
用户可能看到:
程序未响应
如果拆开:
线程 A → UI
线程 B → 下载
界面仍然能够响应。
场景二:处理多个独立任务
例如聊天服务器同时面对:
用户 A
用户 B
用户 C
用户 D
...
如果所有客户端只能排队:
先服务 A
A 完全结束
再服务 B
B 完全结束
...
吞吐能力会非常差。
因此网络服务器天然存在:
并发处理多个连接
的需求。
这也是后面的 Java 网络编程为什么会再次遇到多线程。
场景三:利用多核 CPU
如果存在多个彼此独立的 CPU 密集型任务:
计算 A
计算 B
计算 C
计算 D
合理并行可能充分利用多个 CPU 核心。
场景四:重叠等待时间
大量程序并不是一直计算。
例如:
读取文件
访问数据库
发送网络请求
等待用户输入
都可能发生等待。
一个线程等待期间,另一个线程可以继续工作。
三、使用方法
本章还不要求你正式创建线程。
当前最重要的是:
看到一个程序需求后,先判断它属于什么执行模型。
3.1 判断是否存在多个独立任务
例如:
上传头像
发送消息
保存日志
接收新消息
这些任务并不一定必须严格按:
1 → 2 → 3 → 4
串行执行。
那么就可能存在并发价值。
3.2 判断任务是不是必须等待
例如:
读取 10 GB 文件
网络下载
数据库查询
Socket 等待消息
如果唯一线程一直等待:
整个程序其他任务也无法继续
这类场景通常需要考虑并发设计。
3.3 判断任务之间是否存在共享数据
例如:
线程 A ─┐
线程 B ─┼→ Account.balance
线程 C ─┘
此时虽然非常适合多线程处理多个用户请求,但同时出现新的问题:
共享数据
+
并发访问
=
线程安全风险
后面会专门学习:
synchronized
Lock
来解决这类问题。
四、原理与进阶
4.1 CPU 调度的基本认识
程序创建出多个可运行线程后,并不意味着程序员可以精确规定:
线程 A 先运行 3 ms
然后 B 运行 5 ms
然后 C 运行 2 ms
真正的执行受到很多因素影响,例如:
- 操作系统调度;
- CPU 核心数量;
- 当前系统负载;
- 线程状态;
- I/O 等待;
- JVM 实现;
- 调度策略。
因此多线程程序的一个基本特征就是:
执行顺序通常具有不确定性。
后面看到:
主线程输出:0
子线程输出:0
子线程输出:1
主线程输出:1
或者:
子线程输出:0
主线程输出:0
主线程输出:1
子线程输出:1
都可能是正常现象。
不要把某一次运行时看到的顺序当成语言规则。
4.2 多线程不一定更快
假设任务只有:
1 + 1
却为了它:
创建线程
调度线程
切换线程
销毁线程
线程管理本身反而产生额外成本。
多线程会带来:
- 线程创建成本;
- 调度成本;
- 上下文切换(Context Switch);
- 内存占用;
- 同步成本;
- 线程安全问题;
- Debug 难度;
- 死锁等并发风险。
所以:
多线程是一种解决并发问题的工具,而不是“让所有代码自动变快”的魔法。
4.3 JDK 21 必要补充:平台线程与虚拟线程
在传统 Java 教程中,经常直接把:
Java Thread
≈
操作系统线程
进行理解。
对于传统平台线程(Platform Thread),这种理解在入门阶段基本可用。
但是在 JDK 21 中,虚拟线程(Virtual Thread)已经成为正式特性。
因此更准确的现代模型是:
Java Thread
├── Platform Thread
└── Virtual Thread
平台线程
平台线程通常由操作系统负责调度,对应传统 Java 多线程学习的主要模型。
本课程接下来通过:
class MyThread extends Thread
创建的就是传统平台线程方向。
虚拟线程
虚拟线程主要由 JVM 调度,并会在平台线程上执行。
因此不能再笼统地说:
“所有 Java Thread 都由操作系统直接调度。”
更准确的说法是:
平台线程
↓
通常由操作系统调度
虚拟线程
↓
由 JVM 调度
↓
在平台线程上执行
本阶段首先掌握经典平台线程、线程安全、线程池等 JavaSE 基础。
虚拟线程属于 JDK 21 很重要的现代并发能力,但不改变我们现在学习的:
进程
线程
并发
并行
任务
共享资源
这些基本概念。
五、实践应用
5.1 Web 服务
一个网站可能同时收到:
请求 A:登录
请求 B:查询文章
请求 C:发表评论
请求 D:上传头像
如果请求全部严格串行处理,服务器吞吐能力会非常有限。
因此服务端开发天然是并发编程的重要领域。
5.2 即时通信系统
后面的局域网即时通信项目会出现:
客户端 A
客户端 B
客户端 C
↓
服务端
服务端不能只服务一个用户。
它必须处理:
多个客户端连接
多个消息
多个 I/O 操作
因此网络编程和多线程会自然组合。
5.3 文件下载
下载程序可能同时:
线程 A → 下载
线程 B → 更新进度
线程 C → 处理用户操作
比所有职责放在一个执行流程中更加合理。
5.4 后台任务
例如:
用户提交订单
↓
立即返回订单结果
同时后台:
├── 写日志
├── 发送消息
└── 执行其他任务
这类系统通常也会涉及异步和并发执行。
六、常见问题
6.1 一个程序就是一个线程吗?
不是。
程序运行后形成进程,一个进程内部可以存在多个线程。
6.2 一个 Java 程序只有 main 线程吗?
不是。
从学习者编写业务代码的角度,前期通常只主动使用 main 线程。
但 JVM 本身还可能存在其他运行时线程。
后续我们还会主动创建更多业务线程。
6.3 并发就是并行吗?
不是。
并发强调:
多个任务在重叠时间范围内推进
并行强调:
多个任务在同一瞬间真正执行
6.4 单核 CPU 能不能并发?
可以。
CPU 可以快速在多个线程之间切换。
6.5 单核 CPU 能不能真正同时执行多个 CPU 指令流?
从单个核心的普通执行模型理解,不能同时执行多个独立线程的普通指令流。
它主要通过快速切换制造宏观上的并发效果。
6.6 多核 CPU 就一定是并行吗?
不一定。
是否真正发生并行还受:
- 可运行线程数量;
- 调度;
- 程序结构;
- 系统负载;
等因素影响。
6.7 线程越多是不是程序越快?
不是。
线程过多甚至可能因为:
调度
上下文切换
内存
锁竞争
导致性能下降。
后面学习线程池时会专门解决:
线程不是越多越好,应该合理管理线程资源。
6.8 并发为什么容易出 Bug?
因为多个线程可能:
同时读取
同时修改
同一份共享数据
最终执行顺序又具有不确定性。
于是程序结果可能依赖执行时机。
这就是后续“线程安全”的核心问题。
七、练习与验收
7.1 知识问答
- 什么是进程?什么是线程?
- 进程和线程之间是什么关系?
- 为什么说线程是程序中的一条执行流程?
- Java 程序中的 main 线程是什么?
- 什么叫单线程程序?
- 什么叫多线程程序?
- 什么是并发?
- 什么是并行?
- 并发与并行最核心的区别是什么?
- 单核 CPU 为什么也能实现并发?
- 多核 CPU 为什么能够实现真正的并行?
- 为什么并发和并行可以同时存在?
- 多线程主要可以解决哪些类型的问题?
- 为什么多线程不一定让程序更快?
- 多线程会引入哪些新的工程问题?
- JDK 21 中平台线程与虚拟线程最基本的区别是什么?
7.2 代码阅读
阅读下面的伪执行模型:
任务 A:读取文件 5 秒
任务 B:计算数据 2 秒
任务 C:发送网络请求 3 秒
回答:
- 如果三个任务严格串行执行,大致是什么执行顺序?
- 哪些任务可能存在等待?
- 如果使用多个线程,哪些任务可以在时间上重叠?
- 多线程一定可以把总耗时变成 5 秒吗?为什么?
7.3 手写代码
暂时不要创建新线程。
请分别写出三个普通 Java 方法:
download()
calculate()
writeLog()
然后:
- 在
main()中依次调用三个方法。 - 画出当前程序唯一的主要执行流程。
- 思考如果
download()阻塞 10 秒,后两个方法什么时候才能执行。 - 写出如果未来拆成多个线程后,你希望得到怎样的执行模型。
7.4 Debug
某同学说:
“我的电脑是 8 核,所以只要写 100 个线程,这 100 个线程就会同时运行。”
请指出其中至少三个概念问题。
另一位同学说:
“并发就是 CPU 一会执行 A、一会执行 B,所以多核 CPU 不存在并发,只有并行。”
判断这句话是否正确,并说明原因。
7.5 综合训练
设计一个“即时通信服务器”的概念模型。
服务器当前连接:
用户 A
用户 B
用户 C
用户 D
请回答:
- 如果服务端只有一条业务执行流程,可能出现什么问题?
- 哪些任务天然适合并发处理?
- 哪些数据可能被多个线程共享?
- 共享数据会引出什么新的风险?
- 为什么后面需要学习线程安全和线程池?
本题暂时不要求编码。
7.6 本章验收
如果能够在不查看资料的情况下完成以下任务,可以认为本章基本掌握:
- 画出
操作系统 → 进程 → 线程的关系图。 - 用自己的语言解释进程与线程。
- 用一个生活案例解释并发。
- 用一个生活案例解释并行。
- 明确说明并发不等于并行。
- 解释为什么多线程可以提高响应能力。
- 解释为什么线程不是越多越好。
- 解释为什么多个线程访问共享资源会产生新的风险。
- 知道 JDK 21 中除了传统平台线程之外还有虚拟线程。