qinghuan
← 计算机基础 / 全部笔记

单元 28 / 计算机基础

P0 · 必会主线L1 · 基础

进程、线程与文件系统:程序运行时依赖什么

从运行中的程序出发,分清进程、线程、并发和并行,再理解路径、文件与资源关闭。

本篇目录 · 按需要跳转

读完这篇,你能做什么

能区分进程与线程,并说明路径、垃圾回收和系统资源释放不是同一问题。

学习理由、准备要求与知识点

为什么学它

操作系统的执行和资源边界解释了 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,不模拟真实内核、文件系统或磁盘性能。

正在加载流程图…

调度模型解释任务推进;Java 状态、操作系统状态和 IO 层次不能直接画等号。
查看流程图源码(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["通知等待完成的调用者"]

按顺序看,每一步做了什么

  1. 区分进程隔离与线程共享。
  2. 判断任务主要等待 CPU 还是 IO。
  3. 用完成条件而非猜测睡眠协调。
  4. 明确文件句柄生命周期与访问边界。
  5. 分析调度成本、等待和持久化证据。

03对照实现代码

线程完成等待如何循环检查条件 (新标签页)↗

Microsoft OpenJDK 17.0.20.1;java.base/java/lang/Thread.java · java.lang.Thread#join(long)

以下为原创 Java 17 教学实现,不是原样 JDK 源码;只保留文中明确讨论的机制,省略优化、兼容及内部接口。

核心机制代码 · java
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 层的协调实现,真正线程创建、调度和终止还涉及虚拟机与操作系统能力。

代码先调用谁,再调用谁

  1. 调用者请求 join
  2. 检查目标线程是否仍存活
  3. 在条件循环中等待并处理剩余超时
  4. 目标终止或等待中断、超时后返回对应控制路径

04动手写一个例子

模拟单 CPU 轮转调度的完成时间

所有任务在零时刻到达,输入非负 CPU 工作量和正时间片,按输入顺序轮转。没有 IO、优先级或切换开销;返回每个任务完成时间,零工作量在时刻零完成。模拟步数最多一百万。

输入 1
工作量[5,1,2],时间片2
输出 1
完成时间[8,3,5]
输入 2
[0,1],时间片3
输出 2
[0,1]
输入 3
[],时间片1
输出 3
[]

队列保存仍有剩余工作量的任务编号。每轮取队首,执行剩余量与时间片中较小者,推进模拟时钟;若还有工作就放回队尾,否则记录完成时刻。保持编号使结果能按原输入顺序返回,而不被实际完成顺序打乱。

零工作任务不入队,否则会产生没有进展的调度步骤。输入工作量复制到剩余数组,避免修改调用者数据。时间相加使用精确 long 加法,越界时失败而不是绕回负时间;模拟时钟仅代表理想工作量累计,不读取真实墙上时间。

复杂度不能只写任务数线性,因为一个长任务可能被时间片切成很多轮。先计算每项需要的轮数并设上限,避免极小时间片与极大工作量导致漫长演示。轮数计算用除法和余数,避免工作量加时间片减一的常见溢出形式。

为什么这样写是对的每轮开始时,队列按下一次运行顺序包含且只包含剩余工作量为正的任务,每个未完成任务恰好出现一次。

时间 O(n+S),S 为各任务所需时间片数之和。额外空间 O(n)。

输入验证与复制线性,每个时间片执行一次队列出入和常数运算;任务可反复入队,S 不必与 n 同阶。

保存为 Main.java,运行 javac --release 17 -encoding UTF-8 Main.java,再运行 java Main。程序包含固定输入自测,断言失败时会抛出错误。

展开收起代码 · Main.java · 完整可运行程序Java · 42 行

长行可在代码区域内左右滚动,按 Tab 可把焦点移入代码区。

Main.java · 完整可运行程序 · Java
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
  • 工作目录和相对路径

重点内容会多练几次。先说清楚怎么做,再试试空数据、出错或条件改变时会发生什么。

先自己做,卡住时记下问题,再回来查。问答题写清“为什么、什么时候适用”;动手题留下输入、输出和运行步骤,方便下次重做。

基础必做

  1. 作业 01原理问答

    一个程序启动两个线程,另启动一个独立 Java 进程。分别画出栈、堆和地址空间的归属,说明单核环境下如何描述它们的并发与并行。

    提交内容
    运行模型图和概念对照。
    怎样算完成
    • 线程共享范围明确
    • 逻辑并发与同时执行区分

    练到的知识点进程地址空间 · 线程执行栈和共享堆 · 并发与并行

  2. 作业 02排错实验

    编写主线程启动短任务并 join 的程序,记录进程号和线程标识;重复启动两次比较标识的生命周期,再测试等待被中断的情况。

    提交内容
    Java 实验、自写完成条件测试和运行记录。
    怎样算完成
    • 等待完成与时间延迟区分
    • 标识不被假定永久唯一

    练到的知识点线程调度与 join · 进程号生命周期

综合应用

  1. 作业 03编程实践

    在本题临时目录创建文件,程序接收相对或绝对路径并报告读取结果;从两个工作目录启动,测试不存在路径、目录代替文件与空文件。

    提交内容
    Java 工具、自写路径和边界测试、运行证据。
    怎样算完成
    • 解析基准可见
    • 存在检查不替代实际打开结果
    • 不读取题目范围外私人文件

    练到的知识点工作目录和相对路径 · 文件元数据与权限

  2. 作业 04设计评审

    日志写入后调用 flush,业务希望“断电也不丢”。列出需要区分的用户缓冲、操作系统和存储层,以及验证这种承诺需要的额外条件。

    提交内容
    写入路径图与可承诺范围。
    怎样算完成
    • 软件调用完成与持久化分开
    • 本地普通测试不冒充断电证明

    练到的知识点缓冲写入和持久化边界

  3. 作业 05面试场景

    进程有两百个线程但 CPU 使用率低,文件读取又偶发失败。设计先收集线程状态、等待资源和文件错误的诊断计划,避免仅增加线程数。

    提交内容
    证据清单和诊断决策树。
    怎样算完成
    • CPU 与阻塞证据分开
    • 文件错误保留类型和时点

    练到的知识点线程执行栈和共享堆 · 并发与并行 · 线程调度与 join · 文件元数据与权限

  4. 作业 06排错实验

    工作线程依据显式练习根目录读取文件,测试用受控信号延迟其结束;主线程使用有时限 join,记录等待结束时线程状态和结果文件状态,再释放信号完成清理。

    提交内容
    Java 实验、自写超时等待/完成/路径检查与运行证据。
    怎样算完成
    • 等待返回与工作完成分别核验
    • 所有文件限定在声明的练习根目录

    练到的知识点线程调度与 join · 工作目录和相对路径

  5. 作业 07代码推演

    线程 A 等 B 完成,B 又等待 A 发出的完成信号。画出等待关系并列出能验证是否存在进展的证据,不真正运行无上限等待。

    提交内容
    等待图和有界诊断方案。
    怎样算完成
    • 各等待条件的满足者明确
    • 等待与计算慢区别可解释

    练到的知识点线程调度与 join

  6. 作业 08设计评审

    命令行程序将输出写入相对路径 reports,用户希望输出固定在项目目录。比较按工作目录、程序目录和显式参数解释路径的合同。

    提交内容
    三种启动样本与路径政策。
    怎样算完成
    • 解析基准对使用者可见
    • 跨机器复现条件完整

    练到的知识点工作目录和相对路径

进阶挑战 · 掌握前置后选做

  1. 作业 09编程实践

    完成队列后:实现有限任务的单 CPU 时间片调度模型,输入任务时长 [3,7,2]、时间片 2,支持零任务与非法时长;输出轨迹与完成时刻,声明模拟假设。

    提交内容
    Java 模型、自写边界与守恒测试和运行证据。
    怎样算完成
    • 总执行量可核对
    • 模型不声称复现真实操作系统
    • 资源和切换成本前提明确

    练到的知识点并发与并行 · 线程调度与 join · 缓冲写入和持久化边界

思维拓展

先写出你的猜想,再用例子或实验验证。答案可以不同,但要说清依据、适用情况,以及还有什么没弄懂。

  1. 作业 10思维拓展 · 条件变化

    程序从手动终端改为系统服务运行,用户身份和工作目录均变化。重新审查任务等待、文件访问和退出条件,提出最少上线前验证。

    提交内容
    环境变化表和受控验收计划。
    怎样算完成
    • 环境事实与应用逻辑分开
    • 不直接沿用交互运行的假设

    练到的知识点线程调度与 join · 工作目录和相对路径

完成后,再检验一次

给每题标记“独立完成 / 查阅后完成 / 尚未完成”。查过资料或没做出的题,记下卡在哪里。隔一天换一组输入再试,能解释结果和出错原因,再把这个知识点标为掌握。

继续阅读

先看能帮你完成当前练习的部分。例子跑通后,再读更深入的解释。

打开资料库,找下一份学习材料 ↗