一句话结论
进程 = 资源与安全边界;线程 = CPU 调度的最小单位。
操作系统负责把 CPU 时间片分配给线程;Java 运行在一个操作系统进程(JVM)里,通过 Java 线程与同步原语来组织并发。
一、操作系统视角:进程与线程的本质
1)进程(Process)
- 资源容器:拥有独立的虚拟地址空间、文件句柄、网络套接字、页表等。
- 隔离性强:一个进程崩溃通常不影响其他进程。
- 通信开销大:跨进程共享数据需 IPC(管道、Socket、共享内存、消息队列、mmap 等)。
- 上下文切换成本高:切换涉及寄存器、内核结构、页表与 TLB,易导致缓存失效。
2)线程(Thread)
- 调度单位:同一进程中的多个线程共享代码段、堆和文件句柄,各自有独立的栈与寄存器上下文。
- 通信轻量:共享同一地址空间,传递对象/指针即可,但需要同步保证一致性。
- 崩溃影响范围大:一个线程非法访问共享内存,可能拖垮整个进程。
- 切换相对便宜:避免了进程级的页表切换,但仍有寄存器/栈切换与缓存抖动。
3)调度与状态(简化)
- 状态:就绪(Ready)、运行(Running)、阻塞/等待(Blocked/Waiting)。
- 调度 :现代 OS 采用抢占式调度,按优先级与时间片轮转,尽量让可运行线程平均占用 CPU。
二、Java 视角:JVM、Java 线程与内存模型
1)JVM 与"Java 进程"
- 启动
java
命令后,JVM 本身就是一个操作系统进程。 - JVM 内部主要内存区域:堆(Heap) 、元空间(Metaspace) 、每个线程独立的栈(Thread Stack) 、程序计数器(PC) 、以及 JIT 编译缓存等。
2)Java 线程与操作系统线程
- 现代 HotSpot 实现:Java 线程与 OS 线程一对一 (1:1)。
创建一个java.lang.Thread
,JVM 在底层创建/映射一个原生线程,由 OS 调度。 - 优先级提示 :
Thread#setPriority
只是建议,具体效果取决于操作系统与运行时限制,不能依赖其实现精确调度。
3)Java 线程状态(Thread.State)
- NEW:已创建未启动。
- RUNNABLE:可运行或正在运行。HotSpot 下,处于某些系统调用(如 I/O)时仍可能显示为 RUNNABLE。
- BLOCKED :等待进入
synchronized
监视器锁。 - WAITING :无限期等待(
Object.wait()
、LockSupport.park()
、无超时join()
)。 - TIMED_WAITING :具超时的等待(
sleep
、有超时的wait/park/join
)。 - TERMINATED:线程执行完毕。
这些是 Java 层的抽象状态,与操作系统线程状态并非一一对应。
4)内存可见性与同步(JMM)
- JMM 三要素:原子性、可见性、有序性。
- happens-before 规则约束读写的可见性与顺序。
- 常见工具:
synchronized
(对象监视器)、volatile
(可见性/禁止重排)、Lock
家族、Atomic*
原子类、LongAdder
、StampedLock
、VarHandle
等。
5)守护线程与用户线程
- 用户线程 存在则 JVM 存活;守护线程(如 GC)不阻止 JVM 退出。
- 关闭服务时记得优雅停止线程池,避免资源泄漏与半途而废的任务。
三、对照表:操作系统 vs Java
维度 | 进程(OS) | 线程(OS) | Java 视角 |
---|---|---|---|
资源边界 | 独立地址空间与句柄 | 共享进程资源 | JVM=OS 进程;线程共享堆/元空间 |
调度单位 | 通常不是 | 是 | Java 线程由 OS 调度(1:1) |
通信方式 | IPC,较复杂 | 共享内存,轻量 | 共享 Java 堆,但需同步 |
崩溃影响 | 影响本进程 | 可拖垮整个进程 | 单线程异常未处理可终止 JVM |
切换成本 | 高 | 较低 | 取决于 OS 切换与缓存友好度 |
隔离/安全 | 强 | 弱 | 通过类加载、模块化与权限补充 |
四、工程选型:用多进程还是多线程?
适合多进程
- 强隔离/安全要求:浏览器标签页、插件/脚本沙箱、运行不可信代码。
- 语言/运行时异构:服务拆分为独立进程(微服务、Sidecar)。
- 稳定性优先:单模块崩溃不影响整体。
适合多线程(单进程内并发)
- 共享内存数据多:在同一 JVM 内高效读写。
- I/O 密集:用线程/异步把阻塞时间让位给其他任务。
- CPU 密集:配合合适的并行度(≈ CPU 核心数)提升吞吐。
大型系统常常 多进程 + 进程内多线程 混合使用:进程边界划清责任与隔离,进程内用线程提升吞吐。
五、实战示例与易错点
1)I/O 密集:线程池释放阻塞时间
java
ExecutorService ioPool = new ThreadPoolExecutor(
0, 200,
60, TimeUnit.SECONDS,
new SynchronousQueue<>());
CompletableFuture<String> f1 = CompletableFuture.supplyAsync(() -> httpGet("A"), ioPool);
CompletableFuture<String> f2 = CompletableFuture.supplyAsync(() -> httpGet("B"), ioPool);
String result = f1.thenCombine(f2, (a, b) -> a + b).join();
要点:使用无界并发要谨慎,建议结合限流与超时,避免把下游打挂。
2)CPU 密集:并行度 ≈ 核心数
scss
ForkJoinPool cpuPool = new ForkJoinPool(Runtime.getRuntime().availableProcessors());
cpuPool.submit(() ->
list.parallelStream().map(Foo::compute).reduce(Integer::sum).orElse(0)
).join();
要点:CPU 密集任务并行度设置过大只会带来 线程争用与上下文切换,不增反降。
3)共享状态的线程安全
java
class Counter {
private final AtomicLong n = new AtomicLong();
void inc() { n.incrementAndGet(); }
long get() { return n.get(); }
}
反例:用 volatile long
做自增并不安全;x++
不是原子操作。
4)锁的选择
- 写多读少:
ReentrantLock
或synchronized
。 - 读多写少:
ReentrantReadWriteLock
/StampedLock
乐观读。 - 高并发计数:
LongAdder
通常优于AtomicLong
。
六、常见误解澄清
- "线程越多越快" :错。线程过多会导致上下文切换、缓存失效与锁竞争。
- "设置高优先级就能抢到 CPU" :不可靠。优先级只是提示,取决于 OS 与策略。
- "加了 volatile 就线程安全" :错。
volatile
只保证可见性与有序性,不保证复合操作原子性。 - "阻塞一定是 BLOCKED 状态" :Java 的 RUNNABLE 可能包含处于某些系统调用中的"阻塞 I/O"。
七、与新特性衔接(延伸)
- 虚拟线程(JDK 21) :由 JVM 进行用户态调度,把大量阻塞型任务"复用"到少量载体线程上,极大降低 I/O 并发的线程成本;共享同样的 Java 内存模型与同步语义,业务代码保持直观的同步风格。
- 适合高并发 I/O 场景,但外部依赖(驱动、框架)需与之兼容并避免意外的"载体线程阻塞"。
八、落地清单(Checklist)
- 明确场景:CPU 密集 or I/O 密集?对隔离/稳定性的要求?
- 线程池参数:核心数、最大数、队列类型、拒绝策略、命名、监控指标。
- 共享数据的并发策略:锁/无锁,粒度与持有时间。
- 观测:超时、熔断、限流、背压、RT/吞吐/错误率、上下文切换与 CPU 利用率。
- 故障演练:死锁检测、OOM 与 StackOverflow 保护、优雅停机。
小结
- 操作系统视角:进程重隔离、线程重调度。
- Java 视角:JVM 是进程;Java 线程与 OS 线程 1:1,借助 JMM 与并发工具保障正确性。
- 工程实践:以隔离划分边界,以线程提升吞吐,用数据与监控校准并行度与锁策略。