
开篇导读进程和线程是操作系统中最核心的两个概念也是后端开发、系统编程、嵌入式开发以及各类大厂面试中必考的高频知识点。很多同学在学习时容易把二者混为一谈或者在面对追问「进程和线程到底有什么区别」「线程切换为什么比进程切换快」「多线程一定比单线程快吗」这类问题时答得不够深入。本文用八股文的方式系统梳理进程和线程相关的全部核心考点涵盖基础概念、生命周期、PCB、线程模型、进程间通信、CPU 调度、同步互斥、信号量、管程、经典同步问题、死锁、线程池、协程以及高频面试题。全文力求覆盖全面、逻辑清晰适合面试前系统复习也适合作为操作系统课程的辅助笔记。建议阅读顺序先理解进程和线程的底层定义再掌握状态转换和调度最后攻克同步互斥与死锁这两个公认的难点。文中所有代码示例都尽量保持简洁方便在面试现场手写。1. 进程的基本概念进程是操作系统进行资源分配和调度的基本单位。简单来说进程就是正在运行中的程序。程序是静态的指令集合存放在磁盘上进程是程序的一次动态执行过程它拥有独立的地址空间、内存、文件描述符等资源。一个进程通常包含以下组成部分代码段Text Segment存放程序的机器指令通常是只读的。数据段Data Segment存放已初始化的全局变量和静态变量。BSS 段存放未初始化的全局变量和静态变量程序加载时会被清零。堆Heap动态内存分配区域由程序员通过 malloc、new 等主动管理。栈Stack存放函数调用栈帧、局部变量、返回值地址等由编译器自动管理。进程具有三个核心特征动态性、并发性和独立性。动态性指进程由创建而生、由调度而执行、由撤销而消亡并发性指多个进程可以在宏观上同时推进独立性指每个进程拥有独立的资源空间互不干扰。面试中常问的一个问题是「程序和进程的区别是什么」标准回答是程序是静态的、存储在磁盘上的指令集合本身没有生命周期进程是程序在内存中的动态执行实体拥有独立的资源和控制块具有生命周期。一个程序可以对应多个进程例如同时打开多个浏览器窗口执行的是同一套程序但它们是不同的进程。2. 进程的状态与生命周期进程从创建到消亡会经历多种状态。经典的进程状态模型包括三态模型、五态模型和七态模型。2.1 三态模型三态模型是最基础的状态划分包含以下三种状态就绪态Ready进程已经获得除 CPU 之外的所有必要资源只要获得 CPU 就可以立即运行。运行态Running进程正在占用 CPU 执行指令。阻塞态Blocked / Waiting进程正在等待某个事件发生例如等待 I/O 完成、等待信号量、等待用户输入此时即使把 CPU 分配给它也无法继续执行。三种状态之间的转换关系如下就绪态进程被调度器选中后进入运行态运行态进程时间片用完或被更高优先级进程抢占时回到就绪态运行态进程发起 I/O 请求或等待某个事件时进入阻塞态阻塞态进程等待的事件发生后回到就绪态。2.2 五态模型五态模型在三态模型的基础上增加了两个状态创建态New进程正在被创建操作系统为其分配 PCB、初始化资源但尚未进入就绪队列。终止态Terminated进程执行完毕或被强制终止操作系统正在回收其资源尚未完全消亡。2.3 七态模型七态模型进一步引入了挂起操作区分了「就绪挂起」和「阻塞挂起」两种状态。挂起通常是因为内存紧张操作系统把某些进程从内存中调出到外存以腾出内存空间。挂起状态的进程暂时不参与 CPU 调度需要被激活后才能重新进入就绪态。面试中常问「进程从运行态能直接变为阻塞态吗能直接变为就绪态吗」答案是运行态可以主动发起系统调用后进入阻塞态运行态可以因为时间片用完或被抢占而进入就绪态。但阻塞态不能直接进入运行态必须先回到就绪态因为阻塞进程虽然等到了事件但仍需要排队等待 CPU。3. 进程控制块 PCB进程控制块Process Control BlockPCB是操作系统用于描述和管理进程的数据结构是进程存在的唯一标志。操作系统通过 PCB 感知进程的存在并通过 PCB 中的信息对进程进行调度、管理和控制。一个典型的 PCB 包含以下四类信息进程标识符信息进程 IDPID、父进程 IDPPID、用户标识符UID、进程组 ID 等。处理机状态信息程序计数器PC、通用寄存器、栈指针、程序状态字PSW等这些信息在上下文切换时需要被保存和恢复。进程调度信息进程状态、优先级、阻塞原因、时间片大小、调度队列指针等。进程控制信息程序和数据的地址、同步与通信机制、资源清单、链接指针、打开的文件列表等。PCB 的组织方式主要有线性方式、链接方式链表和索引方式。现役操作系统通常使用链表把 PCB 组织成不同的队列例如就绪队列、阻塞队列和运行队列。Linux 内核用 task_struct 结构体来表示 PCB每个进程或线程在内核中都对应一个 task_struct。面试高频追问「为什么说 PCB 是进程存在的唯一标志」因为操作系统对进程的一切管理本质上都是通过读取和修改 PCB 来实现的。当 PCB 被创建时进程诞生当 PCB 被销毁时进程消亡。即使进程的代码和数据还在内存中只要 PCB 不存在操作系统就认为该进程已经不存在了。4. 进程的创建、终止与阻塞唤醒4.1 进程的创建进程的创建通常由三种事件触发系统初始化时创建第一个进程如 init 进程或 systemd运行中的进程通过系统调用创建子进程用户主动启动一个应用程序。进程创建流程大致如下申请一个空白的 PCB并为新进程分配唯一的 PID。为新进程分配必要的资源如内存空间、栈空间。初始化 PCB包括设置进程状态为创建态、填充标识信息、设置优先级。将新进程插入就绪队列等待调度执行。4.2 fork 与 exec在类 Unix 系统中创建进程主要依靠 fork 系统调用。fork 会创建一个与父进程几乎完全相同的子进程子进程拥有父进程代码段、数据段、堆和栈的副本。fork 的返回值非常特殊在父进程中返回子进程的 PID在子进程中返回 0。因此可以通过判断返回值让父子进程执行不同逻辑。#include stdio.h #include unistd.h int main() { pid_t pid fork(); if (pid 0) { printf(fork failed\n); } else if (pid 0) { printf(child process, pid %d\n, getpid()); } else { printf(parent process, child pid %d\n, pid); } return 0; }exec 系列系统调用则用于替换当前进程的映像即用一个新的程序覆盖当前进程的代码段、数据段、堆和栈但 PID 保持不变。典型的用法是「先 fork 再 exec」父进程 fork 出一个子进程子进程调用 exec 执行新程序父进程则继续自己的逻辑。这种组合方式也是 shell 执行命令的底层原理。现代 Linux 的 fork 使用写时复制Copy On WriteCOW技术优化。fork 时并不真正复制父进程的内存而是让父子进程共享同一份物理内存并把这些内存页标记为只读。当任一进程试图修改某个页面时才触发缺页异常内核为该页复制一份副本。这样既保证了父子进程的独立性又大幅减少了 fork 的开销。4.3 进程的终止进程终止的常见原因包括正常执行完毕调用 exit 主动退出被信号杀死发生致命错误如段错误、除零错误被其他进程调用 kill 终止。进程终止后操作系统会回收其占用的资源、关闭打开的文件、释放内存并最终删除 PCB。这里有一个重要的知识点是僵尸进程和孤儿进程。僵尸进程是指子进程已经结束但父进程没有调用 wait 或 waitpid 回收它的状态导致子进程的 PCB 仍然残留在系统中。僵尸进程本身不占用内存和 CPU但会占用 PID 资源大量僵尸进程可能导致系统无法创建新进程。孤儿进程是指父进程先于子进程结束此时子进程会被 init 或 systemd 进程收养由它负责后续的回收工作因此孤儿进程一般不会造成资源泄漏。4.4 进程的阻塞与唤醒进程的阻塞是进程自身的主动行为。当进程发起 I/O 请求、等待某个事件或者请求的资源暂时不可用时它主动调用阻塞原语把自己的状态从运行态改为阻塞态并把 PCB 插入对应的阻塞队列然后触发一次调度让出 CPU。进程的唤醒则由其他进程或系统事件触发唤醒原语会把目标进程从阻塞队列中移出将其状态改为就绪态并插入就绪队列。5. 线程的基本概念线程是操作系统进行调度的最小单位也被称为轻量级进程Lightweight ProcessLWP。一个进程可以包含多个线程这些线程共享进程的地址空间和大部分资源但每个线程拥有独立的程序计数器、寄存器和栈。引入线程的主要原因是为了提高系统的并发性和响应能力。传统进程模型下进程既是资源分配单位又是调度执行单位创建、销毁和切换进程的开销都比较大。如果把进程比作一个车间线程就是车间里的工人多个工人可以在同一个车间里共享工具和材料并发地完成不同任务。线程和进程一样也有就绪、运行、阻塞等基本状态也有自己的控制块 TCBThread Control Block用于保存线程标识、程序计数器、寄存器状态、栈指针和线程优先级等信息。在多线程程序中多个线程共享进程的堆、全局变量、静态变量、打开的文件和地址空间但每个线程的栈是独立的。这意味着线程之间的通信非常方便直接读写共享变量即可但也带来了数据竞争和同步问题必须借助锁、信号量等机制来保证正确性。6. 进程与线程的核心区别「进程和线程的区别」是八股文中的经典题目几乎是应届生面试的必考题。建议从以下几个维度全面作答对比维度进程线程资源分配资源分配的基本单位拥有独立地址空间和系统资源不拥有系统资源共享所属进程的资源调度传统上是调度的基本单位现代操作系统中是 CPU 调度的基本单位地址空间每个进程拥有独立的虚拟地址空间同一进程的多个线程共享地址空间创建和销毁开销开销大需要分配独立资源开销小只需分配栈和少量私有数据切换开销需要切换页表、刷新 TLB开销大同进程内切换不需要切换地址空间开销小通信方式需要借助 IPC 机制如管道、消息队列、共享内存直接读写共享变量即可但需要同步机制健壮性一个进程崩溃不会影响其他进程一个线程崩溃可能导致整个进程崩溃独立性独立性强互不干扰独立性弱相互影响除了表格中的要点还应该回答为什么线程的创建、销毁和切换开销更小。原因在于创建线程不需要重新分配整个地址空间只需分配栈和少量控制结构同进程线程切换时页表基址寄存器不需要改变TLB 缓存依然有效而进程切换则需要切换页表并导致大量 TLB 失效线程间通信直接访问共享内存不需要经过内核的系统调用。面试官还可能追问「进程切换一定比线程切换慢吗」这里需要注意如果线程属于同一个进程切换时不需要切换地址空间确实更快但如果讨论的是内核级线程线程切换本质上是内核调度切换 task_struct开销仍然存在只是少了地址空间切换这一部分。7. 用户级线程与内核级线程根据线程的实现位置线程可以分为用户级线程User-Level ThreadsULT和内核级线程Kernel-Level ThreadsKLT。这两种实现方式各有优劣是八股文的重要考点。7.1 用户级线程用户级线程完全在用户空间实现由用户态的线程库如早期的 POSIX Pthreads 用户态实现负责创建、调度和管理。内核完全感知不到用户级线程的存在它只看到一个普通的进程。用户级线程的优点包括线程切换不需要陷入内核开销极小。调度算法可以由应用自定义灵活性强。不依赖操作系统内核可以在不支持线程的操作系统上实现。用户级线程的致命缺点包括如果某个线程发起阻塞式系统调用整个进程都会被阻塞因为内核不知道其他线程的存在无法调度执行它们。多个用户级线程无法真正并行运行在多核处理器上因为内核只管理和调度进程。线程切换时需要手动保存和恢复上下文实现复杂。7.2 内核级线程内核级线程由操作系统内核直接创建、调度和管理。内核为每个线程维护独立的 TCB线程切换由内核完成。Windows 的线程和现代 Linux 的线程都属于内核级线程。内核级线程的优点包括多核处理器上可以实现真正的并行执行。一个线程阻塞不影响同进程的其他线程。内核可以直接管理线程的调度和优先级。内核级线程的缺点是线程的创建、销毁和切换都需要进入内核态系统调用开销较大。7.3 用户级线程与内核级线程对比对比维度用户级线程内核级线程实现位置用户空间线程库操作系统内核内核感知内核不可见内核可见且可管理切换开销小无需陷入内核大需要内核参与阻塞系统调用会阻塞整个进程只阻塞当前线程多核支持不能真正并行可以真正并行调度粒度应用自定义内核统一调度8. 多线程模型在实际系统中用户级线程和内核级线程通常组合使用形成不同的多线程模型。主要有一对一模型、多对一模型和多对多模型三种。8.1 多对一模型多个用户级线程映射到一个内核级线程。线程管理在用户空间完成效率高但一个线程阻塞会导致整个进程阻塞且无法利用多核并行。早期的 Green Threads 就采用了这种模型。8.2 一对一模型每个用户级线程都映射到一个内核级线程。这种模型真正实现了并行一个线程阻塞不影响其他线程但每创建一个用户线程都需要创建对应的内核线程开销较大。Linux 的 pthread、Windows 线程都属于这种模型。8.3 多对多模型多个用户级线程以多路复用的方式映射到较少数量的内核级线程。这种模型兼顾了并发能力和系统开销既允许应用创建大量用户线程又不会给内核造成过大压力。同时当一个用户线程阻塞时其他用户线程可以被调度到其他内核线程上执行。不过多对多模型实现复杂度较高目前主流商用系统的应用相对较少。Java 的线程模型在不同平台和版本上有所差异。在 Linux 平台上HotSpot JVM 的 Java 线程通常一对一映射到内核线程即通过 pthread 库创建。也正因为如此Java 线程的创建和切换开销都相对较高高并发场景下后来发展出了线程池、虚拟线程协程等优化方案。9. 进程间通信 IPC由于每个进程拥有独立的地址空间进程之间不能直接访问对方的数据因此需要借助操作系统提供的进程间通信Inter-Process CommunicationIPC机制来交换数据。这是面试中极其重要的考点务必掌握每种方式的原理、优缺点和适用场景。9.1 管道Pipe管道是一种半双工的通信方式数据只能单向流动。管道本质上是一个内核缓冲区通信双方分别从管道的读端和写端进行操作。管道分为匿名管道和命名管道。匿名管道通常用于父子进程或兄弟进程之间的通信因为它没有名字只能通过继承文件描述符的方式传递。命令ps aux | grep java中的竖线就是匿名管道的典型应用。命名管道FIFO在文件系统中有对应的路径名任意两个不相关的进程也可以通过它通信。管道的缺点在于它是半双工的需要双向通信时就得创建两条管道同时管道内的数据是无格式的字节流接收方需要自行解析消息边界。9.2 消息队列Message Queue消息队列是存放在内核中的消息链表每个消息都有固定的格式包括类型和数据。发送进程把消息放入队列接收进程从队列中取走消息。消息队列克服了管道只能传输无格式字节流的问题支持消息的边界和类型区分同时消息队列可以双向通信且不要求通信双方同时在线。消息队列的缺点是消息有大小限制且消息从用户态拷贝到内核态存在拷贝开销。与管道类似消息队列的容量也受内核限制不适合传输大数据量。9.3 共享内存Shared Memory共享内存是最快的 IPC 方式没有之一。它允许多个进程把同一块物理内存映射到各自的虚拟地址空间中之后这些进程就可以像访问普通内存一样直接读写共享数据不需要经过内核中转。共享内存的缺点是它本身不提供任何同步机制多个进程同时读写同一块内存会产生数据竞争因此通常需要配合信号量或互斥锁使用。9.4 信号量Semaphore信号量本质上是一个计数器用于解决进程间的同步和互斥问题。它既可以作为同步工具也可以作为通信工具。信号量的操作包含 P 操作等待和 V 操作信号P 操作会尝试把信号量减一如果结果小于零则阻塞当前进程V 操作会把信号量加一并唤醒一个等待的进程。关于信号量的详细原理后文会有专门章节展开。9.5 信号Signal信号是一种异步通信机制用于通知目标进程发生了某个事件。例如用户按下 CtrlC 会向进程发送 SIGINT 信号Kill 命令可以向进程发送 SIGKILL 或 SIGTERM 信号段错误会触发 SIGSEGV 信号。进程可以注册信号处理函数来响应信号也可以选择忽略某些信号。信号承载的信息量很小主要用于事件通知不适合传输大量数据。9.6 套接字Socket套接字是网络通信的基石也是进程间通信的一种重要方式。与上述几种本机 IPC 不同套接字不仅能用于本机进程之间的通信还能用于不同主机之间的网络通信。套接字基于 TCP 或 UDP 协议通信双方通过 IP 地址和端口号进行标识。9.7 IPC 方式对比总结通信方式数据量是否需内核中转是否支持同步典型场景管道小是数据拷贝两次自带流式同步父子进程、命令管道消息队列中小是数据拷贝两次自带消息边界需要消息类型区分的场景共享内存大否直接映射需额外同步机制高频大数据量交互信号量极小是本身即同步工具资源计数、进程互斥信号极小是异步通知异常通知、外部控制套接字大是依赖协议网络通信、分布式系统10. 上下文切换上下文切换Context Switch是指 CPU 从一个进程或线程切换到另一个进程或线程执行的过程。切换前操作系统必须保存当前任务的执行现场也就是上下文包括程序计数器、寄存器内容、栈指针等切换后再恢复下一个任务的现场使其从上次中断的位置继续执行。进程上下文切换的完整流程大致如下保存当前进程的 CPU 寄存器状态到其 PCB 或内核栈中。更新当前进程的 PCB将其状态从运行态改为就绪态或阻塞态并移动到相应队列。从就绪队列中选出下一个要运行的进程更新其 PCB 状态为运行态。切换到新进程的地址空间更新页表基址寄存器并刷新 TLB。恢复新进程保存的 CPU 寄存器状态跳转到其 PC 指向的位置继续执行。上下文切换本身是纯开销切换期间 CPU 不执行用户程序操作系统还要消耗时间保存和恢复现场。因此频繁的上下文切换会严重拖累系统性能。面试中常问「什么情况下会触发上下文切换」主要包括时间片耗尽、当前进程主动阻塞如 I/O 等待、sleep、更高优先级的进程就绪被抢占、系统调用结束需要重新调度、发生硬件中断且中断处理改变了调度状态等。另一个高频追问是「系统调用会发生上下文切换吗」需要区分两种切换。系统调用会触发用户态到内核态的切换这称为模式切换Mode Switch不是上下文切换Context Switch。模式切换不需要保存和恢复完整的进程现场也不改变进程。但在某些情况下系统调用返回前如果需要重新调度例如当前进程的时间片已用完则会发生真正的进程上下文切换。所以准确的说法是系统调用一定会发生用户态与内核态的切换但不一定发生进程上下文切换。11. CPU 调度算法CPU 调度是操作系统的核心功能之一。当多个进程或线程同时就绪时调度器必须决定把 CPU 分配给谁。调度算法的评价指标通常包括CPU 利用率、系统吞吐量、周转时间、等待时间、响应时间以及公平性。11.1 先来先服务FCFS先来先服务按照进程到达就绪队列的先后顺序进行调度。它的优点是实现简单、公平缺点是短进程可能排在长进程后面长时间等待产生「护航效应」且对交互式系统不友好。11.2 短作业优先SJF短作业优先选择预计运行时间最短的进程先执行。SJF 能获得最小的平均等待时间但需要预知进程的运行时间这在现实中很难做到。更严重的问题是长作业可能被无限期推迟产生饥饿现象。SJF 分为非抢占式和抢占式抢占式 SJF 也叫最短剩余时间优先SRTF。11.3 优先级调度优先级调度为每个进程分配一个优先级每次选择优先级最高的进程执行。优先级可以是静态的也可以是动态调整的。低优先级进程可能长期得不到 CPU产生饥饿。解决饥饿的常见方法是老化Aging即随着等待时间增加逐步提高进程的优先级。11.4 时间片轮转RR时间片轮转专为分时系统设计。所有就绪进程排成一个队列调度器每次把队首进程取出执行一个时间片时间片用完后就把它放到队尾然后调度下一个进程。RR 保证了响应时间适合交互式系统但进程的切换开销与时间片大小相关时间片太小会导致频繁切换执行效率下降时间片太大则会退化为 FCFS。11.5 多级反馈队列MLFQ多级反馈队列是实际操作系统中最常用的综合调度算法它结合了优先级、时间片轮转和老化等多种思路。系统维护多个不同优先级的就绪队列优先级越高的队列时间片越短。新进程先进入最高优先级队列如果在一个时间片内执行不完就降级到下一级队列CPU 优先调度高优先级队列只有高优先级队列为空时才调度低优先级队列。此外为了防止低优先级队列饥饿系统会定期把所有进程重新提升到最高优先级队列。Linux 的 CFS 调度器虽然不是严格意义上的 MLFQ但在设计思路上也有类似的多队列思想。11.6 Linux 常用调度器Linux 历史上使用过 O(1) 调度器后来被完全公平调度器Completely Fair SchedulerCFS取代。CFS 的核心思想不是固定优先级而是尽量保证每个进程获得公平的 CPU 时间。CFS 使用红黑树组织就绪进程以虚拟运行时间vruntime为键值每次选择 vruntime 最小的进程运行。vruntime 增长越慢的进程越容易获得 CPU从而实现了基于权重的公平分配。12. 进程同步与互斥在多进程或多线程环境中多个执行流可能同时访问共享资源。如果对这些共享资源的访问不加控制就会产生数据不一致、逻辑错乱等严重问题。这就是并发编程中的同步与互斥问题。这里先明确几个核心概念临界资源一次只允许一个进程访问的共享资源如共享变量、打印机、共享文件。临界区Critical Section进程中访问临界资源的那段代码。互斥Mutual Exclusion保证同一时刻只有一个进程进入临界区访问临界资源。同步Synchronization多个进程之间按照某种先后顺序协调执行例如生产者必须先生产消费者才能消费。临界区问题的解法必须满足四个条件互斥同一时刻最多有一个进程在临界区内。前进Progress如果没有进程在临界区且存在想进入临界区的进程则必须能选出一个进程让它进入不能无限拖延。有限等待Bounded Waiting一个进程从提出进入请求到获准进入的时间不能无限长必须存在上界防止饥饿。让权等待进程如果不能进入临界区应该立即释放 CPU不能忙等忙等只浪费 CPU 且不推进系统状态但纯软件方案往往无法做到这一点。软件方法最经典的是 Peterson 算法它通过两个共享标志位和一个 turn 变量来解决两个进程的互斥问题。硬件方法则包括中断屏蔽、TestAndSet 指令和 Swap 指令。现代操作系统通常不直接使用这些底层方法而是在其基础上构建信号量、管程等高级同步原语。13. 信号量机制信号量Semaphore由荷兰计算机科学家 Dijkstra 提出是一种功能强大的同步工具既能解决互斥问题也能解决同步先后顺序问题。信号量本质上是一个受保护的整数变量其值只能通过 P 操作和 V 操作来改变。P 操作原语 wait也叫 down 或 acquire把信号量的值减一。如果减一后的值小于零则当前进程阻塞进入该信号量的等待队列。V 操作原语 signal也叫 up 或 release把信号量的值加一。如果加一后的值小于等于零说明有进程正在等待该信号量则唤醒等待队列中的一个进程。信号量按照用途可以分为两类互斥信号量初值为 1用于实现进程间的互斥访问。P 操作相当于加锁V 操作相当于解锁。同步信号量初值为 0 或某个正整数用于控制进程之间的执行顺序。例如生产者生产出数据后执行 V 操作消费者的 P 操作就能通过从而保证消费者不会在数据生产出来之前执行。下面给出一个用互斥信号量保护临界区的伪代码示例semaphore mutex 1; void access_critical_resource() { P(mutex); // 申请进入临界区 // 临界区访问共享资源 V(mutex); // 释放临界区 }信号量的一个重要特点是P 操作和 V 操作都必须是原子操作不能被中断打断。在实现上单核系统可以通过关中断来保证原子性多核系统则需要借助硬件提供的原子指令如 CAS、TestAndSet或自旋锁来保证。信号量的缺点是使用不当容易出错。例如忘记执行 V 操作会导致死锁P 操作位置放错会导致死锁或逻辑错误程序员必须自行保证 P 和 V 的成对出现和正确顺序。为了降低使用难度后来发展出了管程机制。14. 管程管程Monitor是一种更高级的同步机制由 Hoare 和 Hansen 提出。管程把共享资源以及对该资源的所有操作封装在一个模块内部模块外的进程只能通过管程提供的接口来访问共享资源而且管程保证任何时刻只有一个进程能在管程内执行。这样就避免了程序员手动放置 P、V 操作的复杂性从机制上降低了出错概率。管程由四部分组成共享变量管程内部保护的临界资源。条件变量Condition Variable用于实现进程在特定条件下的等待和唤醒。入口队列等待进入管程的进程队列。条件等待队列因条件不满足而阻塞的进程队列。条件变量的两个关键操作是 wait 和 signal。当一个进程在管程内执行时如果发现某个条件不满足就执行条件变量的 wait 操作释放管程的控制权并进入该条件变量的等待队列当另一个进程修改条件后执行 signal 操作唤醒等待队列中的一个进程。关于 signal 之后的管程控制权归属有两种经典语义Hoare 语义要求 signal 后立即把控制权交给被唤醒的进程signal 的调用者需要额外等待Hansen 语义则要求 signal 调用者继续执行直到退出管程被唤醒的进程之后才能继续。Java 的 synchronized 关键字和 wait、notify、notifyAll 方法在语义上更接近 Hansen 管程。notify 只唤醒一个线程且不释放锁notifyAll 会唤醒所有等待线程让它们重新竞争锁因此 Java 中更推荐使用 notifyAll 以避免信号丢失问题。15. 经典同步问题经典同步问题是八股文面试的重灾区尤其是生产者消费者问题几乎人手必会。下面逐一介绍。15.1 生产者消费者问题问题描述一组生产者进程不断生产产品放入缓冲区一组消费者进程不断从缓冲区取出产品。缓冲区大小为 n当缓冲区满时生产者必须等待缓冲区空时消费者必须等待。同时多个生产者和多个消费者对缓冲区的访问必须互斥。解决该问题需要三个信号量互斥信号量 mutex 初值为 1表示空缓冲区数量的 empty 信号量初值为 n表示满缓冲区数量的 full 信号量初值为 0。semaphore mutex 1; semaphore empty n; semaphore full 0; void producer() { while (1) { produce_item(); P(empty); // 先申请空位 P(mutex); // 再进入互斥区 put_item(); V(mutex); // 先退出互斥区 V(full); // 再增加满位计数 } } void consumer() { while (1) { P(full); // 先申请满位 P(mutex); // 再进入互斥区 take_item(); V(mutex); // 先退出互斥区 V(empty); // 再增加空位计数 consume_item(); } }这里有一个非常经典的追问「P 操作的顺序能不能反过来先 P(mutex) 再 P(empty)」答案是不能。如果生产者先获取互斥锁再申请空位当缓冲区满时生产者会拿着 mutex 阻塞在 empty 上消费者又因为拿不到 mutex 无法进入缓冲区消费系统进入死锁。所以正确的顺序是先申请资源信号量再申请互斥信号量释放时则先释放互斥信号量再释放资源信号量。15.2 读者写者问题问题描述多个读者可以同时读共享数据但写者与写者之间、写者与读者之间必须互斥。根据对读者和写者优先级的处理分为读者优先、写者优先和公平竞争三种变体。读者优先的经典解法使用一个 readcount 变量记录当前读者数量并用 mutex 保护 readcount用 rw 信号量保护数据本身。第一个读者进入时对 rw 执行 P 操作最后一个读者离开时对 rw 执行 V 操作从而保证写者只在没有读者时才写。这种方案的缺点是如果读者源源不断写者就会饥饿因此叫读者优先。semaphore mutex 1; semaphore rw 1; int readcount 0; void reader() { P(mutex); readcount; if (readcount 1) { P(rw); // 第一个读者锁住数据 } V(mutex); read_data(); P(mutex); readcount--; if (readcount 0) { V(rw); // 最后一个读者释放数据 } V(mutex); } void writer() { P(rw); write_data(); V(rw); }写者优先的解法通常引入写者计数器和额外的阻断信号量让后来的读者在已有写者等待时不能进入从而避免写者饥饿。公平竞争的解法则统一排队让读者和写者按到达顺序获得访问权可以用读写锁或条件变量配合 FIFO 队列实现。15.3 哲学家进餐问题问题描述五位哲学家围坐在圆桌旁每两位哲学家之间放着一根筷子。哲学家需要同时拿到左右两根筷子才能进餐进餐结束后放下筷子。如果每个哲学家都先拿起左边的筷子再等待右边的筷子就会形成循环等待导致死锁。常见的解法有三种限制同时进餐人数最多允许四位哲学家同时拿筷子保证至少有一位哲学家能拿到两根筷子完成进餐。奇偶编号策略奇数号哲学家先拿左边再拿右边偶数号哲学家先拿右边再拿左边打破循环等待。同时拿起两根筷子只有当左右两根筷子都空闲时才一起拿起否则一根都不拿通过互斥实现原子获取。15.4 吸烟者问题吸烟者问题是生产者消费者问题的变体常被用来考察信号量的熟练程度。桌上有三个吸烟者他们分别拥有烟草、纸和火柴三种材料中的一种。供应者每次随机放两种材料到桌上拥有剩下一种材料的吸烟者才能拿材料卷烟。解题关键在于用三个同步信号量分别对应三种组合供应者根据放下的材料组合执行对应的 V 操作唤醒对应吸烟者。16. 死锁死锁Deadlock是并发编程中最严重的错误之一。死锁发生时两个或多个进程互相等待对方释放资源导致所有相关进程都无法继续推进且永远无法自行解除。16.1 死锁产生的四个必要条件面试必背的四条互斥条件资源一次只能被一个进程占用。如果资源可以被共享就不会发生死锁。请求与保持条件进程已经占有了至少一个资源又提出了新的资源请求而该资源被其他进程占用此时请求进程被阻塞但又不释放已占有的资源。不可剥夺条件进程已获得的资源在使用完之前不能被其他进程强行夺走只能由占有者主动释放。循环等待条件存在一个进程等待环路P0 等 P1 手里的资源P1 等 P2 手里的资源最终 Pn 又等 P0 手里的资源。只有四个条件同时满足才会发生死锁。因此只要破坏其中任意一个条件就能预防死锁。这一点是回答「如何预防死锁」的核心逻辑。16.2 死锁的处理策略处理死锁有四种基本策略预防、避免、检测与恢复、鸵鸟策略忽略。预防Prevention通过破坏四个必要条件之一来杜绝死锁。破坏互斥条件通常不现实因为很多资源本质上就是互斥的破坏请求与保持条件可以要求进程一次性申请所有资源或者申请新资源前先释放已有资源破坏不可剥夺条件可以允许系统强制回收资源破坏循环等待条件可以给所有资源编号要求进程按编号递增的顺序申请资源。避免Avoidance在分配资源之前先判断这次分配是否会导致系统进入不安全状态。最著名的算法是银行家算法。银行家算法要求进程事先声明最大资源需求系统维护可用资源、已分配资源和剩余需求三个矩阵。每次分配前系统先模拟分配然后执行安全性检查如果存在一个安全序列可以让所有进程按顺序执行完毕则这次分配是安全的否则拒绝分配。银行家算法的复杂度较高现实中应用有限。检测与恢复Detection and Recovery允许死锁发生系统定期通过资源分配图检测是否存在环路一旦发现死锁就通过撤销进程、回滚进程或强制剥夺资源等方式恢复。资源分配图检测法的思路是如果图中不存在环则一定没有死锁如果存在环且每种资源只有一个实例则一定发生死锁如果每种资源有多个实例则存在环是死锁的必要不充分条件。鸵鸟策略操作系统假装死锁不会发生不做任何处理。很多通用操作系统采用这种策略因为死锁发生的概率低而预防和检测的开销又比较大系统重启和个人重启进程通常是更经济的恢复方式。16.3 死锁与饥饿的区别死锁是多个进程互相等待形成闭环谁也动不了是「僵持」状态饥饿是一个进程长期得不到所需资源或 CPU是「单个进程被冷落」的状态。死锁一定涉及多个进程饥饿可以只有一个进程死锁中的进程处于阻塞状态饥饿中的进程可能一直在就绪队列里反复错过调度解除死锁通常需要外部干预而饥饿可以通过老化等调度策略缓解。活锁则是另一种情形进程虽然没有阻塞但一直重复无意义的动作始终无法推进本质上也是一种资源分配问题。17. 线程池线程池Thread Pool是实际工程中最重要的线程管理手段。由于线程的创建和销毁开销较大如果每个任务都创建一个新线程在高并发场景下系统会频繁分配和回收线程资源性能急剧下降。线程池通过预先创建一批工作线程并复用它们把任务提交与任务执行解耦从而显著降低系统开销。线程池的核心参数通常包括核心线程数、最大线程数、空闲线程存活时间、任务队列和拒绝策略。以 Java 的 ThreadPoolExecutor 为例它的工作流程如下提交任务后如果当前线程数小于核心线程数则创建新线程执行任务。如果线程数已达到核心线程数任务被放入阻塞队列等待。如果队列已满但线程数小于最大线程数则创建非核心线程执行任务。如果线程数达到最大线程数且队列已满则触发拒绝策略。常见的拒绝策略有四种AbortPolicy 直接抛异常CallerRunsPolicy 由提交任务的线程自己执行DiscardPolicy 静默丢弃任务DiscardOldestPolicy 丢弃队首最老的任务然后重试提交。线程池大小的设置没有固定公式需要根据任务类型调整。CPU 密集型任务通常设置线程数为 CPU 核数加一避免过多线程造成频繁切换I/O 密集型任务因为线程大部分时间在等待 I/O可以设置更多线程常用估算公式为线程数等于 CPU 核数乘以1 加平均等待时间除以平均计算时间再结合压测结果微调。18. 协程协程Coroutine是近年来高并发领域的热点Java 的虚拟线程Virtual Thread、Go 的 goroutine、Python 的 asyncio、Kotlin 的协程都是它的具体实现。协程可以理解为用户态的轻量级线程它由程序自身调度而不是由操作系统内核调度。协程与线程相比有几个显著优势创建开销极小一个协程通常只占几 KB 的栈空间普通线程则要分配几 MB 栈。切换开销极小协程切换在用户态完成不需要陷入内核也不需要切换地址空间。数量优势单台机器上可以轻松创建几十万甚至上百万个协程而线程数量通常受限于内存和内核调度能力。协程的核心机制是在 I/O 等待时主动让出执行权并保存当前上下文等 I/O 就绪后再恢复执行。这种「协作式」调度与线程的「抢占式」调度有本质区别协程必须主动让出否则它会一直占用执行权线程则可以由操作系统强制剥夺 CPU。这也意味着协程更适合 I/O 密集型的并发场景对于 CPU 密集型的计算任务协程并不能真正并行加速还需要配合线程池使用。面试中常问「协程和线程的区别」建议从调度者用户态调度与内核态调度、资源占用小栈与大栈、切换开销无需内核陷入与需要内核陷入、任务抢占协作式与抢占式、适用场景高并发 I/O 与通用并行计算等维度展开。19. 高频面试题汇总这一节把前面所有知识点浓缩成高频面试问答方便快速背诵。19.1 进程和线程的区别是什么从资源分配、调度单位、地址空间、切换开销、通信方式、健壮性六个方面回答详见第 6 节表格。19.2 为什么线程切换比进程切换快因为同进程线程共享地址空间切换时不需要更换页表基址寄存器TLB 缓存依然有效而进程切换必须切换地址空间导致大量 TLB 条目失效内存访问效率骤降。同时线程切换需要保存和恢复的上下文也相对更少。19.3 进程有哪些状态它们之间如何转换至少回答三态模型就绪、运行、阻塞。就绪经调度进入运行运行因时间片耗尽回到就绪运行因等待事件进入阻塞阻塞因事件完成回到就绪。再补充五态模型中的创建态和终止态。19.4 什么是僵尸进程什么是孤儿进程子进程结束后父进程未调用 wait 回收留下僵尸进程占用 PID 资源父进程先结束子进程成为孤儿进程被 init 或 systemd 收养并负责回收。19.5 进程间有哪些通信方式管道、消息队列、共享内存、信号量、信号、套接字。补充说明各自特点和共享内存最快的原因。19.6 什么是死锁产生死锁的四个必要条件是什么死锁是多进程互相等待对方资源导致的僵持状态。四必要条件是互斥、请求与保持、不可剥夺、循环等待。四者缺一不可。19.7 如何预防和避免死锁预防是破坏四个必要条件之一如一次性申请全部资源、资源有序分配、允许剥夺等。避免是动态判断分配安全性典型算法是银行家算法。19.8 什么是临界区进入临界区需要满足什么条件临界区是访问临界资源的代码段。需要满足互斥、前进、有限等待并尽量做到让权等待。19.9 信号量和管程有什么区别信号量是低层原语P 和 V 操作需要程序员手动分布容易出错管程是高层抽象把共享数据和对数据的操作封装在一起由编译器或运行时自动保证互斥降低了使用难度。Java 的 synchronized 就是管程的典型实现。19.10 什么是上下文切换什么场景会触发上下文切换是 CPU 从一个任务切换到另一个任务时保存和恢复现场的过程是纯开销。触发场景包括时间片耗尽、主动阻塞、被高优先级进程抢占、调度点等。注意区分用户态内核态切换与进程上下文切换。19.11 多线程一定比单线程快吗不一定。多线程带来并行收益的同时也引入了线程创建、切换、同步和缓存一致性的开销。对于 CPU 密集型的计算任务在核数足够的情况下多线程确实可能加速对于 I/O 密集型任务多线程可以通过并发等待显著提高吞吐但如果线程数过多、任务粒度过小、锁竞争严重或存在缓存伪共享多线程反而可能比单线程慢。19.12 什么是伪共享如何避免伪共享False Sharing是 CPU 多级缓存机制下出现的性能问题。CPU 缓存以缓存行为单位加载数据通常为 64 字节。如果两个线程频繁修改位于同一缓存行的不同变量即使它们互不相关也会导致缓存行在 CPU 核心之间反复失效和同步严重拖慢性能。避免伪共享的方法包括把频繁修改的独立变量分散到不同缓存行填充 padding、使用内存对齐、Java 中可以使用 Contended 注解JEP 142等。19.13 什么是锁乐观锁和悲观锁有什么区别悲观锁假定冲突一定会发生每次访问共享数据前都先加锁典型实现是 synchronized 和数据库的行锁。乐观锁假定冲突概率低先直接操作提交时再检查是否有冲突典型实现是 CAS 和数据库版本号机制。悲观锁适合写多读少、冲突激烈的场景乐观锁适合读多写少、冲突较少的场景。19.14 什么是 CAS它有什么问题CASCompare And Swap是乐观锁的基础原子操作包含三个操作数内存地址、期望值和更新值。只有当内存地址当前值等于期望值时才把它更新为更新值否则不做任何修改。CAS 的问题包括ABA 问题值从 A 变 B 又变回 ACAS 无法察觉中间变化可用版本号或时间戳解决自旋开销高竞争激烈时 CPU 空转只能保证单个变量的原子性不能保证代码块的原子性。20. 总结进程和线程是操作系统的基石也是所有并发编程知识的源头。回顾全文掌握下面的主线就能应对绝大多数八股文面试进程是资源分配的基本单位线程是 CPU 调度的基本单位二者在资源、地址空间、切换开销、通信和健壮性上有本质区别。进程和线程都有就绪、运行、阻塞等状态进程的生命周期由 PCB 承载PCB 是进程存在的唯一标志。进程间通信有管道、消息队列、共享内存、信号量、信号和套接字六大方式共享内存最快但需要额外同步。上下文切换是影响性能的重要开销要区分模式切换和上下文切换。CPU 调度算法的核心追求是吞吐、响应和公平之间的平衡实际系统常用多级反馈队列或 CFS 之类的综合方案。同步互斥解决的是共享资源的正确访问问题信号量和管程是两大核心工具生产者消费者、读者写者和哲学家进餐是三个经典问题。死锁产生的四个必要条件缺一不可处理策略包括预防、避免、检测与恢复以及鸵鸟策略。线程池和协程是工程中控制并发成本的利器分别从复用线程和用户态轻量调度两个方向进行优化。建议在理解这些概念之后用自己熟悉的语言把生产者消费者、死锁检测、线程池和简单的协程调度各实现一遍再把每一节的面试题用自己的话复述出来。只有把原理转化为能写、能讲、能调的知识才能在面试中从容应对追问。祝大家面试顺利早日拿下心仪的 offer。