并发基础
欢迎来到第七阶段——并发编程。这是整个 Java 教程里最深、最容易踩坑、也最能拉开工程师段位的阶段。前面六章我们写的代码都是”一个人干活”:主线程从上到下依次执行,世界井然有序。但真实世界的程序几乎都要”同时做多件事”——服务器要同时响应成千上万个请求、桌面应用要在后台加载图片的同时响应用户点击、大数据处理要把任务拆成千万份并行计算。
并发,就是 Java 处理”同时做多件事”的核心能力。但要驾驭它,你必须先理解它的本质——而不是只会 new Thread().start()。这一章我们从最基础的概念讲起,把并发的世界观先搭起来。
一、进程与线程:从工厂到工人
要理解线程,先得理解进程。
进程(Process) 是操作系统资源分配的最小单位。每个进程有自己的独立内存空间、文件描述符、网络端口等资源。你可以把进程想象成一座工厂——它有自己的厂房、仓库、电力,与别的工厂物理隔离。一个进程不能直接访问另一个进程的内存,这就是为什么一个程序崩了不会拖垮另一个程序。
线程(Thread) 是 CPU 调度的最小单位。一个进程可以包含多个线程,它们共享进程的内存空间和资源。线程就是工厂里的工人——他们共用同一座厂房和仓库,但每个人有自己的工作台(栈空间、寄存器、程序计数器)。工人之间可以协作,但也会抢同一台机器、同一份原料。
1.1 进程 vs 线程对照
| 维度 | 进程 | 线程 |
|---|---|---|
| 资源 | 独立内存空间 | 共享进程内存 |
| 创建开销 | 大(几 MB 内存、系统调度) | 小(KB 级栈空间) |
| 通信方式 | 管道、消息队列、共享内存、Socket | 直接读写共享变量 |
| 切换成本 | 高(需切换内存空间) | 低(同进程内切换) |
| 崩溃影响 | 进程崩了不影响别的进程 | 线程崩了可能拖垮整个进程 |
| 数量上限 | 几十到几百(一般应用) | 几百到几千(平台线程) |
关键洞察:线程之间共享内存——这是并发的”双刃剑”。一方面让通信极快(直接读变量),另一方面也是一切并发问题的根源:多个线程同时改一个变量,会乱。
1.2 Java 线程与操作系统线程
Java 的线程(在 JDK 21 之前)是”一对一”映射到操作系统原生线程的——也就是平台线程(Platform Thread)。每创建一个 Java 线程,操作系统就分配一个 OS 线程。这意味着线程的创建、销毁、切换都要陷入内核,代价不小。这也是为什么我们不能无限制地创建线程——几万个线程会把系统资源吃光。
JDK 21 正式引入的虚拟线程(Virtual Thread) 改变了这一点——它把 Java 线程从 OS 线程上”解耦”了,几百万个虚拟线程跑在少量平台线程上。我们会在第 45 章详细讲。
二、并发 vs 并行:一种容易混淆的区别
这两个词在中文里都叫”同时执行”,但在计算机科学里它们含义不同。
并发(Concurrency):多个任务在同一时间段内被处理,但同一时刻可能只执行一个。CPU 通过时间片轮转让多个任务”看起来同时”——单核 CPU 上跑 100 个线程就是并发。
并行(Parallelism):多个任务在同一时刻真正同时执行。多核 CPU 上 4 个核同时跑 4 个线程,这才是并行。
打个比方:
- 并发 是一个厨师同时做三道菜——他一会儿切菜、一会儿炒肉、一会儿熬汤,在三个任务间快速切换,让你感觉三道菜”同时在进展”。
- 并行 是三个厨师各做一道菜——真正同时开工。
Rob Pike(Go 语言之父)的名言:“并发是关于处理很多事情,并行是关于做很多事情。” 并发是结构(structure),并行是执行(execution)。一个程序可以并发但不并行(单核上跑多线程),也可以并行但不并发(多核上跑一个任务)。
2.1 为什么并发是必要的
即使你只有一个 CPU 核心,并发依然有价值:
- 资源利用率——一个线程等磁盘 IO 时,另一个线程可以跑 CPU。CPU 不会干等。
- 响应性——GUI 程序里,主线程响应用户操作,后台线程加载图片。即使单核也能”不卡”。
- 公平性——多个用户/请求能轮流被服务,而不是一个长任务阻塞所有人。
- 简化某些设计——有些问题(如服务器)天然就是”同时处理多个独立连接”,用并发模型描述最自然。
并行则是为了性能——把一个大任务拆成几份在多核上同时跑,缩短总时间。
三、并发的收益与代价
并发不是银弹。它有收益,也有沉重的代价。
3.1 收益
- 更高的吞吐量——多线程充分利用多核 CPU。
- 更好的响应性——长任务放后台,主线程保持响应。
- 更优的资源利用——IO 等待时让 CPU 干别的事。
- 更自然的建模——服务器”一个连接一个线程”模型直观。
3.2 代价
- 复杂性爆炸——多线程程序的执行顺序不可预测,bug 难以复现。一个只在百万分之一概率下出现的竞态,可能让你 debug 一周。
- 线程安全开销——加锁、CAS、内存屏障都有性能成本。错误的同步会让多线程跑得比单线程还慢。
- 上下文切换成本——CPU 从一个线程切到另一个,要保存/恢复寄存器、刷新缓存。频繁切换是性能杀手。
- 死锁、活锁、饥饿——并发特有的”陷阱”,单线程程序永远不会遇到。
- 内存可见性——一个线程改了变量,另一个线程可能看不到。这是 JMM(Java 内存模型)要解决的核心问题。
黄金法则:能用单线程解决的问题,就不要用多线程。并发的复杂性只有在收益确实大于代价时才值得付出。
四、两种并发模型:共享状态 vs 分离状态
并发编程的两大流派,本质区别在于”线程之间如何通信”。
4.1 共享状态(Shared State)并发
多个线程共享同一块内存,通过读写共享变量来通信。这是 Java 传统的并发模型。
线程 A ──┐
├──► 共享变量 counter ──◄── 线程 B
线程 C ──┘
优点:通信快(直接读写内存)、与硬件模型契合。
缺点:需要同步(锁、CAS)来避免数据竞争,同步带来复杂性和性能损耗。
Java 里的 synchronized、ReentrantLock、AtomicInteger、ConcurrentHashMap 都是这套模型的工具。
4.2 分离状态(Separate State)并发
每个线程有自己的私有状态,线程之间不共享可变数据,而是通过消息传递(Message Passing) 来通信。
线程 A (私有状态) ──消息──► 线程 B (私有状态)
优点:天然无数据竞争(不共享就不需要锁),更容易推理。
缺点:消息传递有开销,某些场景下不如共享内存直接。
这套模型的代表是 Actor 模型(Akka、Erlang)和 CSP 模型(Go 的 channel)。Java 在 JDK 21 的结构化并发和虚拟线程的推动下,也在向”更分离状态、更少共享可变”的方向演进。
4.3 不可变:两种模型的桥梁
不可变对象(Immutable Object)是个有趣的”中间地带”——它即使被多个线程共享,也不会有数据竞争(因为根本改不了)。所以”共享不可变”是线程安全的。这就是为什么 String、Integer、LocalDate 这些不可变类在并发环境下天然安全。
第 36 章我们会深入讲”线程安全”,第 38 章讲怎么用 synchronized/volatile 保护共享可变状态。
五、Java 并发发展史:从裸线程到虚拟线程
Java 的并发能力不是一天建成的,它经历了近 30 年的演进,每一代都解决了上一代的痛点。
5.1 1995-2004:裸线程时代(JDK 1.0 - 1.4)
Java 从 1.0 起就内建了线程支持——java.lang.Thread、Runnable、synchronized、wait/notify。这在当时是革命性的:C++ 要靠操作系统 API 写多线程,而 Java 把它做成语言特性。
但这一代的工具很原始:你只能手动 new Thread().start(),要管线程生命周期、要自己用 synchronized 保护每一处共享数据。线程池、并发容器、原子类——统统没有。
5.2 2004:JUC 革命(JDK 5)
JDK 5(由 Doug Lea 主导的 JSR-166)引入了 java.util.concurrent(简称 JUC)包——这是 Java 并发的分水岭。一次性带来了:
- Executor 框架——线程池,告别手动
new Thread。 - Lock 接口——
ReentrantLock,比synchronized更灵活。 - 原子类——
AtomicInteger等,基于 CAS 的无锁编程。 - 并发容器——
ConcurrentHashMap、CopyOnWriteArrayList、BlockingQueue。 - 同步器——
CountDownLatch、CyclicBarrier、Semaphore。 - Future——异步任务的”承诺”。
JUC 让 Java 从”能用线程”变成”能工程化地用线程”。Doug Lea 几乎以一己之力把 Java 并发拉到工业级。
5.3 2011:Fork/Join 框架(JDK 7)
JDK 7 引入了 ForkJoinPool——专门为分治任务设计的线程池。它能把一个大任务递归拆成小任务,工作窃取(Work-Stealing)算法让空闲线程偷别的线程队列里的任务,提高 CPU 利用率。这是 parallelStream 的底层引擎。
5.4 2014:CompletableFuture(JDK 8)
JDK 8 带来了 CompletableFuture——终于让 Java 有了可组合的异步编程。原来的 Future 只能阻塞 get(),不能链式回调。CompletableFuture 支持链式编排、异常处理、多任务组合——一举把 Java 异步编程拉到与 JavaScript Promise、C# Task 同一水平。
5.5 2017-2021:反应式与渐进改进(JDK 9-17)
这几版陆续带来了 Flow API(反应式编程规范)、VarHandle(更底层的内存操作)、增强的 ForkJoinPool。但反应式编程门槛高、调试难,业界一直呼唤更简单的方案。
5.6 2023:虚拟线程与结构化并发(JDK 21)
JDK 21 正式发布虚拟线程(Virtual Thread,JEP 444)——这是 Java 并发 20 年来最大的变革。
虚拟线程让 Java 回归”一个请求一个线程”的简单模型,但不再受 OS 线程数量限制。一个服务器可以同时跑几百万个虚拟线程,处理几百万个连接,代码却写得像传统阻塞式 IO 那样简单。它把 Go 语言协程的优雅带回了 Java。
同时引入的结构化并发(Structured Concurrency,JEP 453,预览) 和 Scoped Values(JEP 446,预览) 进一步规范了并发任务的生命周期管理。
5.7 时间线一览
| 年份 | JDK | 里程碑 | 核心能力 |
|---|---|---|---|
| 1996 | 1.0 | Thread/synchronized | 裸线程 |
| 2004 | 5 | JUC 包 | 线程池、并发容器、原子类 |
| 2006 | 6 | java.util.concurrent 扩展 | 更完善的并发工具 |
| 2011 | 7 | Fork/Join | 分治与工作窃取 |
| 2014 | 8 | CompletableFuture | 可组合异步 |
| 2017 | 9 | Flow API | 反应式规范 |
| 2017-21 | 9-17 | VarHandle、增强 | 底层内存操作 |
| 2023 | 21 | 虚拟线程、结构化并发 | 海量轻量级线程 |
六、实战:用两种模型演示并发
下面这个例子同时演示”共享状态”和”分离状态”两种并发模型——用 4 个线程累加到 100 万。共享状态用 AtomicLong,分离状态用每个线程独立累加最后汇总。
观察重点:
- 分离状态模型明显更快——每个线程操作自己的本地变量,没有 CAS 争用,CPU 缓存也不会失效。
- 共享状态模型的结果是对的(1000000),但付出了 CAS 重试的代价。
- 这种”本地累加 + 最后汇总”的思想,正是
LongAdder的设计原理(第 41 章会讲)。Runtime.getRuntime().availableProcessors()返回可用 CPU 核数——决定并行度的关键参数。
七、本章小结
| 概念 | 核心要点 |
|---|---|
| 进程 vs 线程 | 进程是资源单位(工厂),线程是调度单位(工人);线程共享进程内存 |
| 并发 vs 并行 | 并发 = 同时处理(结构),并行 = 同时执行(执行);单核可并发,多核才能并行 |
| 并发收益 | 吞吐量、响应性、资源利用、建模自然 |
| 并发代价 | 复杂性、同步开销、上下文切换、死锁风险、可见性问题 |
| 共享状态模型 | 多线程读写共享变量,需同步(Java 传统模型) |
| 分离状态模型 | 线程私有状态 + 消息传递,无数据竞争(Actor/CSP) |
| 不可变 | 两种模型的桥梁——共享不可变天然安全 |
| Java 并发史 | Thread(1996) → JUC(2004) → Fork/Join(2011) → CompletableFuture(2014) → 虚拟线程(2023) |
记忆口诀:
- 进程是工厂,线程是工人——工人共用厂房,所以协作快但容易冲突。
- 并发是切换,并行是真同时——单核也能并发,多核才能并行。
- 共享快但要锁,分离安全但要传话——各有取舍。
- 不可变是免锁的金钥匙——能不可变就不可变。
结语:这一阶段我们要走的路
并发是 Java 最深的领域之一。这一阶段我们用 12 章把它从地基到塔尖讲一遍:
- 第 35 章 线程基础——怎么创建、启动、控制线程。
- 第 36 章 线程安全——为什么多线程会出问题,怎么定义”安全”。
- 第 37 章 Java 内存模型——JMM 是并发正确性的理论基础。
- 第 38 章 同步机制——
synchronized/volatile/ThreadLocal/wait/notify。 - 第 39 章 并发典型问题——死锁、饥饿、活锁,那些年踩过的坑。
- 第 40 章 锁与同步器——
ReentrantLock、读写锁、StampedLock、Semaphore、CountDownLatch等。 - 第 41 章 原子类与 CAS——无锁编程的基石。
- 第 42 章 并发集合——
ConcurrentHashMap、CopyOnWriteArrayList、阻塞队列。 - 第 43 章 线程池——
ThreadPoolExecutor七参数、拒绝策略、ForkJoinPool。 - 第 44 章 CompletableFuture——异步编程的瑞士军刀。
- 第 45 章 虚拟线程与结构化并发——Java 并发的未来。
每一章都是后一章的基石。打牢基础,后面的”高深”内容才不会成为空中楼阁。准备好了吗?让我们从下一章的”线程基础”开始,真正动手写并发代码。