进程与线程的区别?

2026年 阅读约 undefined 分钟 undefined

进程是资源分配的基本单位,线程是CPU调度的基本单位。本文从定义、私有与共享资源、上下文切换开销、通信方式四个维度对比进程与线程,讲清为什么有了进程还要有线程、多线程一定更快吗、协程与虚拟线程是什么,附Java实战代码与面试高频问答。

一句话总结

进程是资源分配的基本单位,线程是 CPU 调度的基本单位。一个进程里可以跑多个线程,它们共享堆、全局变量和文件描述符,但各有自己的栈、程序计数器和寄存器;因此创建、切换、销毁线程的成本远低于进程。通信上,进程之间要靠管道、消息队列、共享内存等 IPC,线程之间直接读写共享变量即可,但必须处理同步与可见性问题。

初级理解

核心区别对比

维度进程线程
定位资源分配的基本单位CPU 调度的基本单位
地址空间独立,互相隔离同进程内共享
私有资源整个地址空间栈、程序计数器、寄存器
共享资源无(除非显式 IPC)堆、全局变量、文件描述符
创建/切换开销大(要建/换页表)小(同地址空间,TLB 不用刷)
通信方式管道、消息队列、共享内存、信号直接共享内存(需同步)
崩溃影响只挂自己整个进程一起挂

一个进程内的多个线程

# Java:同一进程内的两个线程共享静态变量 public class SharedDemo { static int counter = 0; // 堆上,线程间共享 public static void main(String[] args) throws Exception { Thread t1 = new Thread(() -> { for (int i = 0; i < 100000; i++) counter++; }); Thread t2 = new Thread(() -> { for (int i = 0; i < 100000; i++) counter++; }); t1.start(); t2.start(); t1.join(); t2.join(); System.out.println(counter); // 往往小于 200000:counter++ 不是原子操作 } } # 结论:共享带来方便,也带来竞争——这就是 synchronized / volatile / 原子类存在的意义

中级深入

上下文切换:线程为什么比进程轻

# 进程切换: # 1. 保存当前进程的寄存器、程序计数器 # 2. 切换页表(虚拟地址空间变了)→ TLB 失效,后续访存要重新建立映射 # 3. 刷新各种缓存相关状态 # 线程切换(同进程内): # 1. 保存/恢复该线程私有的栈、寄存器、程序计数器 # 2. 页表不变、TLB 不失效 → 省掉最贵的一步 # 所以:线程切换通常比进程切换快一个数量级; # 但切换仍然不免费(微秒级 + 缓存污染),无脑开线程反而更慢

为什么有了进程还要线程

# 1. 并发:一个进程内的任务需要"同时"推进(浏览器一边渲染一边下载) # 2. 通信成本:进程间 IPC 要走内核、序列化,线程间读写共享变量几乎零成本 # 3. 创建开销:频繁创建销毁的任务(如每个请求一个线程)用进程代价无法接受 # 4. 多核利用:进程也能用多核,但内存不共享,协作麻烦

多线程一定更快吗

# 不一定,分场景: # 1. CPU 密集型:核数固定时,线程数超过核数只会增加切换开销,不会更快 # (Python 还有 GIL,同进程多线程连一个核都用不满) # 2. IO 密集型:等待时让出 CPU,多线程/多协程能显著提升吞吐 # 3. 有共享竞争的:加锁会让并行退化成串行,甚至比单线程还慢 # 4. Amdahl 定律:可并行比例决定加速上限

高级进阶

用户态线程与内核态线程

# 1:1(内核线程):每个线程都是一个内核调度实体,Linux pthread、Java Thread # 优点:可利用多核;缺点:创建切换要走内核,数量受限(万级) # N:1(用户态线程):多个用户线程映射到一个内核线程,早期 Java 绿线、部分协程实现 # 优点:切换在用户态完成,极轻量;缺点:一个线程阻塞系统调用,整组全挂 # M:N(混合):Go 的 goroutine(g + m + p 模型)——海量用户态协程 + 少量内核线程

协程与虚拟线程

# 协程(协作式):遇到 IO 主动让出,由运行时库调度,不走内核 # Go:go func() 一行启动一个 goroutine,默认并发几十万 # Java 21 虚拟线程(Loom):写起来像阻塞式,运行时挂起时不占内核线程 try (var executor = Executors.newVirtualThreadPerTaskExecutor()) { for (int i = 0; i < 100_000; i++) { executor.submit(() -> { Thread.sleep(Duration.ofSeconds(1)); return null; }); } } // 10 万个"并发睡眠",传统线程池早就 OOM 了

线程池与 IO 多路复用

# 线程不是越多越好:每个线程有栈(默认 1MB 起)+ 调度成本 # 经验值:CPU 密集型 = 核数 + 1;IO 密集型 = 核数 / (1 - 阻塞系数) # IO 多路复用是另一条路:一个线程用 epoll 盯住成千上万连接(Redis、Nginx、Netty) # select/poll/epoll 演进:epoll 只返回就绪的 fd,不遍历全量

实战场景

# 场景1:选进程还是选线程? # 需要强隔离、容错(一个挂了不影响别人)→ 多进程:Chrome 每标签一进程 # 需要高频通信、共享大量数据 → 多线程:Web 服务器 worker # 既要隔离又要通信 → 多进程 + 共享内存 / 消息队列
# 场景2:线程池正确用法 ThreadPoolExecutor pool = new ThreadPoolExecutor( 4, // 核心线程 8, // 最大线程 60, TimeUnit.SECONDS, // 空闲回收 new ArrayBlockingQueue<>(200), // 有界队列,拒绝 OOM new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略:回退到调用方 ); # 要点:队列必须有界;拒绝策略要想清楚;Executors.newFixedThreadPool 默认无界队列是坑
# 场景3:线程间共享变量的正确同步 # 可见性 + 原子性,两个问题 private volatile boolean running = true; // 保证可见性 private final AtomicInteger count = new AtomicInteger(); // 保证原子性 # volatile 不保证原子性;synchronized/AtomicXxx/Lock 按场景选

面试模拟

面试官:进程和线程有什么区别?

你:进程是资源分配的基本单位,有独立地址空间;线程是调度的基本单位,是进程内的执行流,共享堆和全局变量,但有自己的栈、程序计数器和寄存器。所以线程创建切换更轻量、通信更方便,但一个线程崩溃会带走整个进程,共享数据也必须处理同步。

面试官:有了进程为什么还要有线程?

你:一是并发:一个程序内部要同时做多件事,比如浏览器一边渲染一边网络下载;二是通信成本:进程间 IPC 要走内核还要序列化,线程间直接读写共享内存;三是开销:高频创建销毁用进程代价太高,比如每个请求一个线程的场景。

面试官:线程切换为什么比进程切换快?

你:同进程的线程共享地址空间,切换时页表不用换,TLB 不会失效,只需保存恢复私有的栈和寄存器;进程切换要换页表,TLB 失效后访存要重新建映射,这是最贵的部分。但线程切换也不是免费的,有寄存器保存和缓存污染,所以线程不是越多越好。