做并发编程的人,早晚都要面对这两个词:进程、线程。不管是面试被反复追问“进程和线程的区别”,还是线上接口突然卡死、CPU飙到100%、线程池队列被打满,追根溯源都会落到这俩概念上。我接触并发编程这么多年,见过太多人把进程和线程的关系停留在“进程是资源分配的单位,线程是调度的单位”这种背课本的层面,真到排查问题的时候,线程状态看不懂、堆栈信息不会抓、线程池参数乱配,最后只能靠重启解决。这篇文章就不讲虚的了,从操作系统底层的视角把进程和线程彻底拆开,再把线程池、并发安全、死锁、进程通信这些实战高频问题逐个过一遍,适合正在学并发的初学者,也适合写了好几年业务代码但一直没系统梳理过并发知识的开发人员。
1. 进程与线程:并发世界的地基
1.1 先搞清一个核心问题:进程和线程到底是什么关系
很多人把进程理解成“正在运行的程序”,这个说法对,但不完整。进程是操作系统进行资源分配和隔离的基本单位,它包含了一段完整的地址空间、打开的文件描述符、环境变量、堆栈、代码段数据段,以及操作系统为它维护的各种控制信息。
线程则是进程内部的一条执行路径,是真正被CPU调度执行的最小单位。同一个进程里的多个线程共享这个进程的地址空间和资源,但各自拥有独立的程序计数器、栈和寄存器状态。
我用一个生活化的类比来解释:把操作系统比作一家公司,进程就是公司里的一个个项目组,每个项目组有自己独立的办公室、设备、预算,组与组之间互不干扰。线程就是项目组里的员工,同一个组的员工共享办公室、设备和预算,但每个员工有自己的工位和手头任务。你要新开一个项目组,公司得批办公室、配设备,成本很高;但在项目组里加一个人,只需要安排一个工位就行,成本低得多。进程切换的代价远大于线程切换,道理就在这里——切换进程要换办公室、换预算表,切换线程只是换个员工干活。
从Linux的实现来看,线程本质上是轻量级进程,通过clone系统调用创建,只是共享了地址空间等资源。JVM里所谓的“用户线程”和操作系统的线程还是一一对应的关系。所以搞清楚这个概念,后续看线程池的线程数设置、进程通信的开销、性能调优的方向,才有依据。
1.2 线程的完整生命周期:从新建到终止
线程不是一new出来就开始跑,也不是一调用start就立刻执行。Java线程有六个状态,这个面试常考,但很多人只是背名字,不理解状态迁移的触发条件。
- NEW:刚new出来,还没调用start,此时线程对象存在,但操作系统里还没有对应线程。
- RUNNABLE:调用了start,线程已经在JVM里就绪,等待操作系统调度分配CPU时间片。这里注意,Java的RUNNABLE把操作系统里的就绪(ready)和运行(running)两个状态合并了。
- BLOCKED:线程在等待获取监视器锁,也就是想进入synchronized代码块但锁被别人占着。
- WAITING:线程在等待其他线程唤醒,比如调用Object.wait()、Thread.join()、LockSupport.park()。
- TIMED_WAITING:带超时等待,比如sleep(1000)、wait(1000)、join(1000)。
- TERMINATED:线程执行完run方法或者抛出未捕获异常退出。
最容易混淆的就是BLOCKED和WAITING。简单区分:BLOCKED是“锁被人占着,我进不去”,是竞争同步锁时特有的状态;WAITING是“我主动歇了,等别人叫我”,通常是调wait或join之后。线上抓线程快照时,如果大量线程卡在BLOCKED,基本可以断定是锁冲突热点;如果大量线程卡在WAITING,多半是线程池队列等待、RPC调用等待之类的场景,要结合堆栈具体看。
1.3 进程和线程的经典对比
既然要“彻底搞懂”,那进程和线程的核心区别必须掰开揉碎。下面这个表我从资源、切换、通信、稳定性、开销几个维度做了对比,实际排查问题和做技术选型的时候,这个表可以直接拿来当参照。
| 对比维度 | 进程 | 线程 |
|---|---|---|
| 资源拥有 | 独立的地址空间、文件、信号等资源 | 共享所属进程的地址空间和资源 |
| 系统开销 | 创建和切换开销大,涉及页表切换、缓存失效 | 创建和切换开销小,栈和寄存器切换为主 |
| 通信方式 | IPC(管道、消息队列、共享内存、Socket等) | 共享内存、wait/notify、Lock等,更直接 |
| 健壮性 | 一个进程崩溃一般不影响其他进程 | 一个线程崩溃可能导致整个进程退出 |
| 适用场景 | 强隔离、多租户、跨语言、分布式节点 | 高并发、任务密集、共享数据频繁的场景 |
这里说一个我踩过坑的点:不要把“线程切换开销小”理解成“线程切换不要钱”。线程切换同样涉及用户
