读完这篇,你能做什么
能区分进程与线程,并说明路径、垃圾回收和系统资源释放不是同一问题。
学习理由、准备要求与知识点
为什么学它
操作系统的执行和资源边界解释了 Java 线程共享、文件路径与关闭责任。
先读这些内容
知识点清单
- 进程地址空间
- 线程执行栈和共享堆
- 并发与并行
- 线程调度与 join
- 进程号生命周期
- 工作目录和相对路径
- 文件元数据与权限
- 缓冲写入和持久化边界
运行前先确认Java 17;源码在本机 Microsoft OpenJDK 17.0.20.1 src.zip 核验,在线链接为上游 jdk-17.0.20-ga。深入模块可在掌握基础后回读。
01先弄懂它是什么
程序运行后,操作系统通常用进程管理它的内存、打开的文件等资源。线程是进程里的一条执行路线:多个线程通常共用进程的内存和资源,但各有自己的调用栈与执行状态。因此一个线程修改共享对象,另一个线程可能受到影响;线程之间并不是天然隔离的。
同时处理多个任务不一定意味着同一瞬间都在运行。并发指任务交错推进,单核也能做到;并行指多个执行单元确实同时工作。多核提供并行条件,但锁、IO 等待和任务之间的依赖仍会限制速度。线程越多也不一定越快,因为切换和竞争要花时间。本篇以 Java17 普通平台线程为范围,不套用后续版本虚拟线程的行为。
文件系统把字节数据组织成文件与目录,并维护名称、元数据和访问规则。路径是寻找对象的描述,不是文件内容本身;打开文件后获得的句柄代表一次访问关系,重命名、删除和共享访问行为还取决于系统规则。磁盘上的文件、操作系统缓存和应用缓冲是不同层次,写调用成功并不自动等于断电后仍可恢复。
文中这几个词,先认一认
- 虚拟地址空间
- 可以把它看成进程使用的一张地址地图:程序按图上的编号访问内存,再由操作系统和硬件把地址对应到实际存储位置。图上的地址不等于机器内存里同号的固定格子。
- 上下文切换
- 处理器先保存当前线程的执行状态,再恢复另一线程的状态并转去运行它。
- 系统调用
- 程序请求操作系统代办文件访问、进程管理等事情的正式入口。
- 文件句柄
- 程序打开文件后得到的访问标识,系统用它找到这次访问的相关状态。
02看看它怎么工作
线程通常在就绪、运行和等待之间变化。需要 CPU 的线程等待调度,等待磁盘或网络的线程可能暂时不占用 CPU 执行业务指令;但具体等待方式和系统实现有关。Java Thread.State 是虚拟机可观察状态分类,不能直接当成操作系统调度器的全部内部状态,也不能只凭一个状态名称推断根因。
等待另一个线程结束时,join 表达的是完成条件,不是猜一个固定 sleep 时间。sleep 一段时间只能推迟当前线程,不能保证目标已经完成。条件等待需要在被唤醒后重新检查,因为通知与条件不是同一事实,还可能存在虚假唤醒。超时等待也应依据单调时间衡量已过去时长,避免墙上时钟调整改变等待预算。
文件 IO 通过用户态库、系统调用、文件系统与设备等层次推进。路径解析、权限检查、缓存命中和真正设备访问可能分别发生,不能把每次 read 都想象成直接访问硬盘。下图把 CPU 调度与 IO 等待分开;后面的轮转算法只模拟一个理想 CPU,不模拟真实内核、文件系统或磁盘性能。
正在加载流程图…
查看流程图源码(Mermaid)
flowchart TD
A["进程:地址空间与资源"] --> B["线程 A 的执行上下文"]
A --> C["线程 B 的执行上下文"]
B --> D["就绪队列等待 CPU"]
C --> D
D --> E["运行一个时间片"]
E --> F{"任务状态"}
F -->|"仍需 CPU"| D
F -->|"等待 IO"| G["系统服务与文件系统"]
G --> D
F -->|"完成"| H["通知等待完成的调用者"]按顺序看,每一步做了什么
- 区分进程隔离与线程共享。
- 判断任务主要等待 CPU 还是 IO。
- 用完成条件而非猜测睡眠协调。
- 明确文件句柄生命周期与访问边界。
- 分析调度成本、等待和持久化证据。
03对照实现代码
Microsoft OpenJDK 17.0.20.1;java.base/java/lang/Thread.java · java.lang.Thread#join(long)
以下为原创 Java 17 教学实现,不是原样 JDK 源码;只保留文中明确讨论的机制,省略优化、兼容及内部接口。
public final class SourceModel {
private boolean completed;
public synchronized void finish() {
completed = true;
notifyAll();
}
public synchronized boolean awaitMillis(long timeout) throws InterruptedException {
if (timeout < 0 || timeout > 86_400_000L) throw new IllegalArgumentException();
long start = System.nanoTime();
long budget = timeout * 1_000_000L;
while (!completed) {
if (timeout == 0) { wait(); continue; }
// 唤醒后重新计算剩余预算,不能无条件再等待整个超时。
long remaining = budget - (System.nanoTime() - start);
if (remaining <= 0) return false;
wait(remaining / 1_000_000L, (int)(remaining % 1_000_000L));
}
return true;
}
}本机 Thread.join(long) 在同步范围内依据 isAlive 循环等待;有超时时使用 System.nanoTime 计算已经过去的时间,再更新剩余毫秒。模型采用独立完成对象,不在 Thread 实例上自行 wait/notify,展示相同的条件循环思想;它不实现真实线程生命周期,也不模拟操作系统调度器。
循环而非单次 if 的原因是唤醒只说明需要重新检查条件。通知可能与期望状态无关,也可能发生虚假唤醒;若醒来就直接宣布完成,会把“停止等待”误当成“任务成功结束”。模型只有 finish 设置完成标记,等待者每次都重新读取受同一锁保护的状态。
超时预算应扣除已经过去的时间,而不是每次被唤醒都重新获得完整超时。nanoTime 用于计算经过时长,不用于展示日历时间;它的绝对数值没有业务日期含义。模型限制最多一天是为了简化纳秒乘法范围,真实 JDK 方法具有自己的参数和边界处理。
InterruptedException 是等待被中断的信号,不表示目标线程已经停止。调用者可以传播中断或按层次恢复中断标记并结束自己的工作,不能吞掉后宣称 join 成功。源码还说明这是 Java 层的协调实现,真正线程创建、调度和终止还涉及虚拟机与操作系统能力。
代码先调用谁,再调用谁
- 调用者请求 join
- 检查目标线程是否仍存活
- 在条件循环中等待并处理剩余超时
- 目标终止或等待中断、超时后返回对应控制路径
04动手写一个例子
模拟单 CPU 轮转调度的完成时间
所有任务在零时刻到达,输入非负 CPU 工作量和正时间片,按输入顺序轮转。没有 IO、优先级或切换开销;返回每个任务完成时间,零工作量在时刻零完成。模拟步数最多一百万。
- 输入 1
工作量[5,1,2],时间片2- 输出 1
完成时间[8,3,5]
- 输入 2
[0,1],时间片3- 输出 2
[0,1]
- 输入 3
[],时间片1- 输出 3
[]
队列保存仍有剩余工作量的任务编号。每轮取队首,执行剩余量与时间片中较小者,推进模拟时钟;若还有工作就放回队尾,否则记录完成时刻。保持编号使结果能按原输入顺序返回,而不被实际完成顺序打乱。
零工作任务不入队,否则会产生没有进展的调度步骤。输入工作量复制到剩余数组,避免修改调用者数据。时间相加使用精确 long 加法,越界时失败而不是绕回负时间;模拟时钟仅代表理想工作量累计,不读取真实墙上时间。
复杂度不能只写任务数线性,因为一个长任务可能被时间片切成很多轮。先计算每项需要的轮数并设上限,避免极小时间片与极大工作量导致漫长演示。轮数计算用除法和余数,避免工作量加时间片减一的常见溢出形式。
为什么这样写是对的每轮开始时,队列按下一次运行顺序包含且只包含剩余工作量为正的任务,每个未完成任务恰好出现一次。
输入验证与复制线性,每个时间片执行一次队列出入和常数运算;任务可反复入队,S 不必与 n 同阶。
保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。
展开收起代码 · Main.java · 完整可运行程序
长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。
import java.util.ArrayDeque;
import java.util.Arrays;
public class Main {
static long[] finishTimes(long[] jobs, long quantum) {
if (jobs == null || quantum <= 0) throw new IllegalArgumentException("input");
long[] remaining = jobs.clone(), finished = new long[jobs.length];
ArrayDeque<Integer> ready = new ArrayDeque<>();
long steps = 0;
for (int i = 0; i < jobs.length; i++) {
if (jobs[i] < 0) throw new IllegalArgumentException("negative job");
long rounds = jobs[i] / quantum + (jobs[i] % quantum == 0 ? 0 : 1);
if (rounds > 1_000_000 - steps) throw new IllegalArgumentException("simulation limit");
steps += rounds;
if (jobs[i] > 0) ready.add(i); // 零工作任务在时刻零完成。
}
long now = 0;
while (!ready.isEmpty()) {
int id = ready.remove();
long run = Math.min(remaining[id],quantum);
now = Math.addExact(now,run); // 不允许模拟时钟溢出。
remaining[id] -= run;
if (remaining[id] == 0) finished[id] = now;
else ready.add(id); // 未完成者排到尾部,维持轮转顺序。
}
return finished;
}
static void check(long[] wanted, long quantum, long... jobs) {
if (!Arrays.equals(wanted,finishTimes(jobs,quantum))) throw new AssertionError();
}
public static void main(String[] args) {
// 普通轮转、零工作、空输入、短作业与最大单任务。
check(new long[]{8,3,5},2,5,1,2);
check(new long[]{0,1},3,0,1);
check(new long[]{},1);
check(new long[]{1,2},1,1,1);
check(new long[]{Long.MAX_VALUE},Long.MAX_VALUE,Long.MAX_VALUE);
boolean rejected = false;
try { finishTimes(new long[]{1},0); } catch (IllegalArgumentException e) { rejected = true; }
if (!rejected) throw new AssertionError("quantum");
System.out.println("PASS 6");
}
}这些特殊情况也要试一试
- 零工作量不入队。
- 时间片必须为正。
- 模拟步数与累计时间分别检查范围。
- 结果按任务原编号返回。
进阶选读 · 可选深入:带 IO 与到达时间的调度
场景任务分时到达,并在 CPU 与 IO 阶段之间切换。
做法增加事件优先队列管理到达和 IO 完成,把就绪队列与等待集合分开;保留切换成本和优先级规则,避免把模型结果当作真实系统性能。
复杂度事件处理通常需要 O(log E) 队列操作,具体总成本取决于事件与时间片数量。
好处与代价模型越详细参数越多,应根据问题选择必要机制并用实际观测校准。
05项目里怎么用
性能排查先观察 CPU 利用率、线程等待、队列长度和 IO 延迟的关系。CPU 空闲但请求慢可能在等待下游,CPU 满也可能忙于竞争或垃圾回收,不能只看线程总数。增加线程有时提高 IO 等待期间的利用率,有时只扩大队列与内存压力,需要控制并发与任务提交速度。
文件更新要先明确两件事:其他程序能否看见写到一半的状态,以及断电后哪些内容必须保留。检查路径到真正打开之间,目标可能变化;重命名能否一次完成,也取决于文件系统与移动范围。临时文件再替换是常用办法,但仍要核对目标系统支持什么,不能把一次 API 返回成功当成所有故障都已处理。
进程与线程隔离还影响故障范围。线程共享内存带来低成本通信,也使共享状态错误波及整个进程;进程隔离更强,但通信、部署与资源协调成本更高。Java 层同步只能管理它覆盖的对象和操作,跨进程文件或数据库的一致性需要对应系统机制。
容易踩的坑
- 把并发等同于多核同时执行。
- 用 sleep 猜测其它线程已经完成。
- 认为线程数量增加必然提高吞吐。
- 把应用 flush、文件写成功和持久化保证混为一谈。
几种做法,怎么选
表格有四列,窄屏可左右滑动查看代价和适用条件。
| 方案 | 优势 | 代价与限制 | 适用条件 |
|---|---|---|---|
| 多线程协作 | 共享内存通信方便 | 竞争、可见性与故障隔离需要管理 | 同一应用内可控并发 |
| 多进程隔离 | 地址空间与生命周期边界较强 | 通信和部署成本增加 | 需要故障隔离或独立服务 |
| 顺序执行 | 行为与资源成本容易推理 | 无法利用可并行工作 | 任务少、依赖强或先求正确的实现 |
06检验是否读懂了
先自己想、自己写,再展开答案核对。场景题是假设的练习情境;遇到还没学过的内容,可以做完基础后再回来。
先用自己的话解释
01线程和进程的主要区别是什么?
展开参考答案与推导
进程通常提供地址空间与资源边界,线程是其中的执行路径并共享进程资源。线程各有执行上下文,进程间通常需显式通信;实际操作系统细节不同,不能只用“轻量进程”四字代替这些关系。
02join 与 sleep 为什么不同?
展开参考答案与推导
join 等待特定线程终止这一条件,sleep 只让当前线程延后继续。睡眠结束不证明目标完成,等待还可能超时或被中断,因此应按返回与条件判断下一步。
03轮转模型为何不代表真实调度结果?
展开参考答案与推导
它假设全部任务同时到达、无 IO 和切换成本,并只模拟单 CPU。真实系统还有优先级、多核、缓存和外部等待,模型用于解释规则,不能直接作为机器实测结论。
换个情况,还会处理吗
01线程很多但 CPU 很低,先排查什么?
展开参考答案与推导
查看是否大量等待网络、磁盘、锁或任务队列,结合请求延迟与下游状态判断。不要立即再加线程;先明确瓶颈和并发上限,避免把等待堆积变成内存压力。
02中断 join 后目标线程仍运行,是错误吗?
展开参考答案与推导
不是。被中断的是等待者的等待操作,目标线程不因此自动停止。若需要取消目标任务,必须采用它支持的协作取消机制,并分别确认取消请求与实际终止。
03文件存在检查通过,打开仍失败,为什么?
展开参考答案与推导
检查与打开不是天然原子操作,期间文件可能移动、权限改变或被其它进程影响。应以实际打开结果为准并处理失败,不能把前置存在检查当作后续访问保证。
写代码试一试
01增加时间片大于所有任务的测试
- 输入 / 条件
工作量[2,3],时间片10- 输出 / 预期
[2,5]
提示 每个任务一次完成。
展开参考答案与推导
加入 check(new long[]{2,5},10,2,3)。它应退化为按输入顺序执行,验证运行量取剩余与时间片的较小值,而非每次无条件增加完整时间片。
02加入固定切换开销
- 输入 / 条件
任务[1,1],时间片1,开销1- 输出 / 预期
先定义首次是否收费
提示 把政策写在模拟契约。
展开参考答案与推导
若规定每次任务开始前都增加一单位开销,完成时间为二和四。先明确首次及同一任务连续轮次如何收费,再修改 now 推进和测试,不能事后为结果选择解释。
03验证不修改输入
- 输入 / 条件
原任务[5,1]- 输出 / 预期
调用后仍为[5,1]
提示 remaining 应是副本。
展开参考答案与推导
保存原数组,调用后用 Arrays.equals 检查未变化。算法内部递减的是 clone 得到的 remaining;若直接复用 jobs,会把调度计算变成调用者看不见的破坏性修改。
读完之后,独立完成
课后作业:自己做一遍
本篇共 10 道作业,先完成 8 道基础与综合任务,再按前置知识挑战进阶题。其中包含 1 道思维拓展,试着换个条件,看看原来的办法是否还管用。不附参考答案。
能区分进程与线程,并说明路径、垃圾回收和系统资源释放不是同一问题。
本篇重点加练
重点加练等待关系与运行目录,帮助解释程序为何在不同启动方式下呈现不同结果。
- 线程调度与 join
- 工作目录和相对路径
重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。
先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。
基础必做
- 作业 01原理问答
一个程序启动两个线程,另启动一个独立 Java 进程。分别画出栈、堆和地址空间的归属,说明单核环境下如何描述它们的并发与并行。
- 提交内容
- 运行模型图和概念对照。
- 怎样算完成
- 线程共享范围明确
- 逻辑并发与同时执行区分
练到的知识点进程地址空间 · 线程执行栈和共享堆 · 并发与并行
- 作业 02排错实验
编写主线程启动短任务并 join 的程序,记录进程号和线程标识;重复启动两次比较标识的生命周期,再测试等待被中断的情况。
- 提交内容
- Java 实验、自写完成条件测试和运行记录。
- 怎样算完成
- 等待完成与时间延迟区分
- 标识不被假定永久唯一
练到的知识点线程调度与 join · 进程号生命周期
综合应用
- 作业 03编程实践
在本题临时目录创建文件,程序接收相对或绝对路径并报告读取结果;从两个工作目录启动,测试不存在路径、目录代替文件与空文件。
- 提交内容
- Java 工具、自写路径和边界测试、运行证据。
- 怎样算完成
- 解析基准可见
- 存在检查不替代实际打开结果
- 不读取题目范围外私人文件
练到的知识点工作目录和相对路径 · 文件元数据与权限
- 作业 04设计评审
日志写入后调用 flush,业务希望“断电也不丢”。列出需要区分的用户缓冲、操作系统和存储层,以及验证这种承诺需要的额外条件。
- 提交内容
- 写入路径图与可承诺范围。
- 怎样算完成
- 软件调用完成与持久化分开
- 本地普通测试不冒充断电证明
练到的知识点缓冲写入和持久化边界
- 作业 05面试场景
进程有两百个线程但 CPU 使用率低,文件读取又偶发失败。设计先收集线程状态、等待资源和文件错误的诊断计划,避免仅增加线程数。
- 提交内容
- 证据清单和诊断决策树。
- 怎样算完成
- CPU 与阻塞证据分开
- 文件错误保留类型和时点
练到的知识点线程执行栈和共享堆 · 并发与并行 · 线程调度与 join · 文件元数据与权限
- 作业 06排错实验
工作线程依据显式练习根目录读取文件,测试用受控信号延迟其结束;主线程使用有时限 join,记录等待结束时线程状态和结果文件状态,再释放信号完成清理。
- 提交内容
- Java 实验、自写超时等待/完成/路径检查与运行证据。
- 怎样算完成
- 等待返回与工作完成分别核验
- 所有文件限定在声明的练习根目录
练到的知识点线程调度与 join · 工作目录和相对路径
- 作业 07代码推演
线程 A 等 B 完成,B 又等待 A 发出的完成信号。画出等待关系并列出能验证是否存在进展的证据,不真正运行无上限等待。
- 提交内容
- 等待图和有界诊断方案。
- 怎样算完成
- 各等待条件的满足者明确
- 等待与计算慢区别可解释
练到的知识点线程调度与 join
- 作业 08设计评审
命令行程序将输出写入相对路径 reports,用户希望输出固定在项目目录。比较按工作目录、程序目录和显式参数解释路径的合同。
- 提交内容
- 三种启动样本与路径政策。
- 怎样算完成
- 解析基准对使用者可见
- 跨机器复现条件完整
练到的知识点工作目录和相对路径
进阶挑战 · 掌握前置后选做
- 作业 09编程实践
完成队列后:实现有限任务的单 CPU 时间片调度模型,输入任务时长 [3,7,2]、时间片 2,支持零任务与非法时长;输出轨迹与完成时刻,声明模拟假设。
- 提交内容
- Java 模型、自写边界与守恒测试和运行证据。
- 怎样算完成
- 总执行量可核对
- 模型不声称复现真实操作系统
- 资源和切换成本前提明确
练到的知识点并发与并行 · 线程调度与 join · 缓冲写入和持久化边界
思维拓展
先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。
- 作业 10思维拓展 · 条件变化
程序从手动终端改为系统服务运行,用户身份和工作目录均变化。重新审查任务等待、文件访问和退出条件,提出最少上线前验证。
- 提交内容
- 环境变化表和受控验收计划。
- 怎样算完成
- 环境事实与应用逻辑分开
- 不直接沿用交互运行的假设
练到的知识点线程调度与 join · 工作目录和相对路径
完成后,再检验一次
给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。
继续阅读
先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。
- 必读教程英文
OSTEP:操作系统导论 (新标签页)↗
按 Virtualization、Concurrency、Persistence 三部分继续阅读进程、线程与文件系统章节。
- 实践参考文档英文
Microsoft Win32:进程与线程 (新标签页)↗
阅读地址空间、线程调度和共享资源,连接 Java 进程与系统观察工具。
- 深入参考文档英文
Microsoft Win32:本地文件系统 (新标签页)↗
阅读文件系统职责与平台差异,理解文件名、目录和存储布局的边界。
- 深入参考文档英文
Java 17 API:ThreadLocal (新标签页)↗
阅读 get
/set/remove的线程绑定规则以及线程池中的清理责任。 - 实践实践指南英文
程序怎样跑起来:CPU、系统调用与任务切换图解 (新标签页)↗
先看目录中 Ch. 1 Basics 和 Ch. 2 Multitasking 的图,画出“运行程序—请求内核帮助—回到程序”和“任务轮流使用 CPU”的过程。能解释这些图后,再按需读程序加载与内存章节。 英文正文公开。文章使用简化模型,主要围绕 Linux 与相关硬件机制;2023 年的调度参数和实现说明不等于当前所有系统的行为。
- 实践实践指南英文
OSTEP 进程短读:准备好了,为什么还没轮到运行 (新标签页)↗
先读开头的程序与进程区别,再读 4.4 的三状态图。设想 A 正在算数、B 发出磁盘请求,在纸上画两条时间线,标出谁运行、谁就绪、谁阻塞;说明 I
/O完成后为什么不一定立刻获得 CPU。把三状态图当成简化教学模型,不当成某个系统完整的进程状态列表。 英文单章 PDF 公开免费;整本合订版和纸书另有付费入口,本次学习不需要购买。已读第 4 章进程定义与 4.4 状态部分,未下载或运行课后模拟器。 - 深入参考文档英文
OpenJDK 17u java.lang.Thread (新标签页)↗
定位 Java17
Thread.join的条件循环、单调时间预算与中断传播;它不是操作系统调度源码。