CodeRoad Map 程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。
由 程序员柏风 独立维护 · 关于作者
学习 更多
操作系统概念
这一课你将能做到
区分进程与线程,解释上下文切换的成本,以及 Java 线程与操作系统线程的关系
描述 CPU 调度、虚拟内存、分页与缺页中断的基本思想
理解文件系统的组织方式,以及互斥锁、信号量、条件变量分别解决什么问题
为什么要学
操作系统是「管家」:一套硬件,几百个程序争抢 CPU、内存、磁盘,它负责让大家「看似同时」运行、互不踩踏。Java 后端遇到的大量线上问题,根因都在这个层面:
接口 RT(响应时间)毛刺:可能是线程被调度挂起、上下文切换、缺页中断
机器内存没满但应用报 OOM:虚拟内存与 JVM 内存限制的交互
线程池开多大?开多了反而更慢:调度与切换成本
磁盘写了一半机器掉电:文件系统与日志(journal)如何保证一致性
判题平台更是操作系统知识的密集场景:每份提交都要隔离执行、限制 CPU/内存/时间、回收工作目录。理解进程线程与调度,才能正确设计执行沙箱和线程池。
先建立图像
进程与线程
┌─────────────── 进程 Process(资源分配单位)──────────────┐
│ 独立的地址空间:代码段/数据段/堆 │
│ ┌线程1(栈+寄存器)┐ ┌线程2(栈+寄存器)┐ ┌线程3┐ │
│ │ 共享进程的堆和文件描述符,各自有栈 │ │
└───────────────────────────────────────────────────────┘
进程间内存隔离(通信要靠管道/Socket/共享内存)
同进程线程共享内存(通信快,但要处理并发安全)
并发「同时」的真相(单核)
线程A ▌▌ ▌▌▌ CPU 在 A/B/C 间快速切换
线程B ▌▌ ▌▌ 时间片 time slice(毫秒级)
线程C ▌▌ ▌▌
虚拟内存
进程看到的:0 ........ 连续的虚拟地址空间
↓ MMU 查页表翻译 ↓
物理内存的散页 / 暂存到磁盘的交换区
跟着做
步骤 1:观察进程和线程
public class ProcessThreadDemo {
public static void main(String[] args) {
long pid = ProcessHandle.current().pid();
System.out.println("当前 JVM 进程 PID = " + pid);
// 一个进程里开两个线程:它们共享 main 里的变量
Thread t1 = new Thread(() -> {
for (int i = 0; i < 3; i++) {
System.out.println("线程A: " + i);
}
}, "thread-A");
Thread t2 = new Thread(() -> {
for (int i = 0; i < 3; i++) {
System.out.println("线程B: " + i);
}
}, "thread-B");
t1.start();
t2.start();
// 输出顺序是交错的、不保证的——这就是并发:调度顺序由操作系统决定
}
}
运行后再开一个终端,Windows 执行 tasklist | findstr java,Linux/macOS 执行 ps -ef | grep java,能看到这个 JVM 进程;JDK 自带的 jstack <pid> 可以列出进程内所有线程及其栈。关键认知:Java 传统线程(java.lang.Thread)在主流 JDK 上一对一映射操作系统内核线程 ,创建、阻塞都有内核开销,所以不能为每个请求新建线程,要用线程池(Java 21 的虚拟线程则是另一套轻量模型)。
步骤 2:体会上下文切换的代价
import java.util.concurrent.CountDownLatch;
public class SwitchCost {
public static void main(String[] args) throws Exception {
int n = 200_000;
// 对照组:两个线程各跑各的
CountDownLatch latch1 = new CountDownLatch(2);
long t1 = System.nanoTime();
for (int t = 0; t < 2; t++) {
new Thread(() -> {
for (int i = 0; i < n; i++) { Math.sin(i); }
latch1.countDown();
}).start();
}
latch1.await();
long t2 = System.nanoTime();
// 实验组:两个线程用一根锁反复交替唤醒(制造大量切换/争用)
final Object lock = new Object();
CountDownLatch latch2 = new CountDownLatch(2);
for (int t = 0; t < 2; t++) {
new Thread(() -> {
for (int i = 0; i < n; i++) {
synchronized (lock) { lock.notifyAll(); } // 频繁让出与争用
}
latch2.countDown();
}).start();
}
latch2.await();
long t3 = System.nanoTime();
System.out.println("无争用(ms): " + (t2 - t1) / 1_000_000);
System.out.println("强争用(ms): " + (t3 - t2) / 1_000_000);
}
}
切换时操作系统要保存当前线程的寄存器、程序计数器、栈指针,装入另一个线程的全套现场,还要刷掉部分 CPU 缓存——纳秒到微秒级,单次不起眼,高并发下累积可观。工程推论:**线程数 ≈ 核数 ×(1 + 等待时间/计算时间)**只是起点,I/O 密集可多开,CPU 密集开到核数附近即可,盲目「开 1000 个线程」会让切换吞掉算力。
步骤 3:并发原语——锁、信号量、条件变量 import java.util.concurrent.Semaphore;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.ReentrantLock;
public class ConcurrencyPrimitives {
// 信号量:判题平台同时只允许 4 个任务执行(许可数=空闲判题机数)
static Semaphore judgeSlots = new Semaphore(4);
// 互斥锁 + 条件变量:任务必须等到有空闲机才能开始
static ReentrantLock lock = new ReentrantLock();
static Condition hasSlot = lock.newCondition();
static int freeMachines = 2;
public static void main(String[] args) throws Exception {
Runnable judge = () -> {
try {
judgeSlots.acquire();
System.out.println(Thread.currentThread().getName() + " 开始判题");
Thread.sleep(100);
judgeSlots.release();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
};
for (int i = 0; i < 10; i++) {
new Thread(judge, "task-" + i).start();
}
Thread.sleep(1000);
}
}
三者分工要记牢:互斥锁 保证同一时刻只有一个线程进入临界区(保护共享数据);信号量 维护 N 个许可,用来限流和资源池管理;条件变量 让线程「等待某个条件成立并被唤醒」,配合锁实现生产者-消费者(判题任务入队、工作线程领取执行就是这个模式,JDK 里对应 BlockingQueue)。
步骤 4:虚拟内存、缺页与文件系统直觉 # Linux 下观察内存与进程
free -h # 物理内存与交换区 swap 使用情况
top # 按 1 看各核;RES 是进程实际占用物理内存
jcmd <pid> VM.native_memory summary # JVM 各区域内存(需开启 NativeMemoryTracking)
# 观察文件:文件系统把「路径名」映射到 inode(元数据+数据块索引)
ls -i # 每个文件前面的数字就是 inode 号
df -i # 看 inode 是否耗尽(小文件海量时会先没 inode)
直觉串讲:每个进程以为自己独占整个内存,实际访问的是虚拟地址 ,MMU 查页表 翻译成物理页;访问的数据不在物理内存(被换到磁盘或从未加载)时触发缺页中断 ,操作系统从磁盘搬一页进来——这是访问变慢上千倍的典型原因。文件系统则用 inode + 数据块管理磁盘文件,写入不是立刻落盘(先有页缓存),崩溃一致性靠日志(journaling,如 ext4)或写时复制(如 ZFS)保证;MySQL 的 redo log、Redis 的 AOF 是同样思想在应用层的重现。
讲清楚要点
进程是资源分配单位,线程是 CPU 调度单位 :进程有独立地址空间,崩溃一般不互相影响;同进程线程共享堆,一个线程出错可能拖垮整个进程,所以判题沙箱必须按进程/容器隔离用户代码。
并发 ≠ 并行 :并发是「处理多个任务的能力」(单核切换即可),并行是「同时执行多个任务」(需要多核);协程/虚拟线程解决的是线程创建与阻塞的开销,不改变 CPU 核数。
调度算法的演进直觉 :时间片轮转保证公平,优先级应对重要任务,多级反馈队列兼顾「短任务快响应、长任务高吞吐」;对 Java 程序员体现为线程优先级几乎不可靠、要靠线程池与队列设计。
虚拟内存的三个收益 :进程隔离与安全、用磁盘扩展内存、让每个进程拥有简单连续的地址空间;代价是翻译开销(TLB 缓存页表项)和缺页中断。
文件系统 = 文件的组织与块管理 :目录也是文件,路径经逐层查找定位 inode;顺序写远快于随机写,这是日志型存储(追加写)和 LSM 树性能优势的物理根源。
你可能会懵的地方
线程数设成 CPU 核数就一定最优吗?
只对 CPU 密集型成立。Tomcat 这类服务大量时间在等数据库和网络(I/O 期间线程挂起不占 CPU),所以工作线程要多于核数;现代做法是用带界线程池配合压测找到拐点,或直接用虚拟线程。
synchronized 等待时线程占着 CPU 吗?
不占。竞争失败的线程会被挂起(进入阻塞态),操作系统把 CPU 分给别的线程;而 CAS 自旋锁会短暂占 CPU 空转,适合等待极短的场景,各有适用面。
虚拟内存「用磁盘当内存」是不是等于免费扩容?
不是。换入换出(swap)走磁盘,频繁换页会让机器慢到像死机(抖动 thrashing)。生产服务器通常限制甚至关闭 swap,应用内存要靠容量规划而不是透支。
为什么写文件后还要 flush/fsync?
write 先进操作系统页缓存就返回了,掉电会丢;flush 把应用缓冲刷给 OS,fsync 才强制落盘。数据库的 WAL 必须 fsync 后才应答客户端,才能在崩溃后恢复。
进程间通信为什么这么「麻烦」?
隔离是安全与稳定的前提,通信必须经过操作系统允许的通道:管道、消息队列、Socket(网络栈)、共享内存(最快但要自己加锁)。微服务之间本质就是走 Socket 的进程间通信。
错法对照 错法 问题 正解 每个请求 new Thread() 内核线程创建贵、数量失控可能拖垮机器 用有界线程池,队列与拒绝策略明确 锁范围包整个大方法 串行化,并发收益归零 只保护共享数据的最小临界区 wait/notify 不放在循环条件里 虚假唤醒导致逻辑错误 永远 while(条件不满足) wait();优先用 Condition/BlockingQueue 写文件不 flush 就假设已持久化 崩溃丢数据 关键路径 fsync,或交给数据库事务 用 Runtime.exec 执行用户命令却不回收进程 进程/文件描述符泄漏 读取输出流、waitFor 超时、destroyForcibly 兜底
卡点急救
CPU 不高但接口很慢 :优先排查锁等待(jstack 看 BLOCKED/WAITING 线程)、I/O 等待、调度延迟,而不是去优化算法。
容器内应用莫名被 OOM Kill :检查 JVM 堆外内存(元空间、线程栈、直接内存、Native 库)总和是否超过容器限制;-XX:MaxRAMPercentage 给整体留余量。
判题机器逐渐卡死 :检查用户进程是否全部被回收(僵尸进程)、工作目录文件是否清理(inode/磁盘)、超时任务是否被真正 kill 掉进程组。
面试常问
进程和线程的区别?什么时候用多进程、什么时候用多线程?
进程独立地址空间、隔离好、开销大、通信要走 IPC;线程共享内存、轻量、通信快但要处理并发安全。需要强隔离与容错(如执行用户代码、Chrome 标签页)用多进程/容器;高并发共享业务数据用多线程。
什么是上下文切换?怎么减少?
切换线程时保存/恢复寄存器、PC、栈指针并可能冷掉缓存的过程。减少方法:控制线程数量、缩小锁粒度减少争用、用无锁结构(CAS/ConcurrentHashMap)、避免频繁阻塞唤醒、I/O 批量化。
虚拟内存解决什么问题?缺页中断为什么影响性能?
解决内存隔离、容量扩展与地址空间简化。访问页不在内存时陷入内核、从磁盘读页,耗时毫秒级(比内存慢数万倍);程序工作集大于物理内存时频繁缺页即抖动,表现为整机卡顿。
自测三问
一张图讲清「进程—线程—栈—堆—文件描述符」的归属关系。
判题平台执行用户提交,分别在哪一步用到了进程隔离、信号量思想和文件系统?
为什么 fsync 很慢却不能省?哪些系统组件必须依赖它?
小结一句话 操作系统用进程隔离资源、用线程调度 CPU、用虚拟内存制造「独占且无限」的内存假象、用文件系统管理持久数据——理解这四件事,线上性能与并发问题就有了操作系统层面的诊断坐标。
延伸