并发基础

欢迎来到第七阶段——并发编程。这是整个 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 核心,并发依然有价值:

  1. 资源利用率——一个线程等磁盘 IO 时,另一个线程可以跑 CPU。CPU 不会干等。
  2. 响应性——GUI 程序里,主线程响应用户操作,后台线程加载图片。即使单核也能”不卡”。
  3. 公平性——多个用户/请求能轮流被服务,而不是一个长任务阻塞所有人。
  4. 简化某些设计——有些问题(如服务器)天然就是”同时处理多个独立连接”,用并发模型描述最自然。

并行则是为了性能——把一个大任务拆成几份在多核上同时跑,缩短总时间。

三、并发的收益与代价

并发不是银弹。它有收益,也有沉重的代价。

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 里的 synchronizedReentrantLockAtomicIntegerConcurrentHashMap 都是这套模型的工具。

4.2 分离状态(Separate State)并发

每个线程有自己的私有状态,线程之间不共享可变数据,而是通过消息传递(Message Passing) 来通信。

线程 A (私有状态) ──消息──► 线程 B (私有状态)

优点:天然无数据竞争(不共享就不需要锁),更容易推理。

缺点:消息传递有开销,某些场景下不如共享内存直接。

这套模型的代表是 Actor 模型(Akka、Erlang)和 CSP 模型(Go 的 channel)。Java 在 JDK 21 的结构化并发和虚拟线程的推动下,也在向”更分离状态、更少共享可变”的方向演进。

4.3 不可变:两种模型的桥梁

不可变对象(Immutable Object)是个有趣的”中间地带”——它即使被多个线程共享,也不会有数据竞争(因为根本改不了)。所以”共享不可变”是线程安全的。这就是为什么 StringIntegerLocalDate 这些不可变类在并发环境下天然安全。

第 36 章我们会深入讲”线程安全”,第 38 章讲怎么用 synchronized/volatile 保护共享可变状态。

五、Java 并发发展史:从裸线程到虚拟线程

Java 的并发能力不是一天建成的,它经历了近 30 年的演进,每一代都解决了上一代的痛点。

5.1 1995-2004:裸线程时代(JDK 1.0 - 1.4)

Java 从 1.0 起就内建了线程支持——java.lang.ThreadRunnablesynchronizedwait/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 的无锁编程。
  • 并发容器——ConcurrentHashMapCopyOnWriteArrayListBlockingQueue
  • 同步器——CountDownLatchCyclicBarrierSemaphore
  • 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里程碑核心能力
19961.0Thread/synchronized裸线程
20045JUC 包线程池、并发容器、原子类
20066java.util.concurrent 扩展更完善的并发工具
20117Fork/Join分治与工作窃取
20148CompletableFuture可组合异步
20179Flow API反应式规范
2017-219-17VarHandle、增强底层内存操作
202321虚拟线程、结构化并发海量轻量级线程

六、实战:用两种模型演示并发

下面这个例子同时演示”共享状态”和”分离状态”两种并发模型——用 4 个线程累加到 100 万。共享状态用 AtomicLong,分离状态用每个线程独立累加最后汇总。

Java · 在线运行

观察重点

  • 分离状态模型明显更快——每个线程操作自己的本地变量,没有 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、读写锁、StampedLockSemaphoreCountDownLatch 等。
  • 第 41 章 原子类与 CAS——无锁编程的基石。
  • 第 42 章 并发集合——ConcurrentHashMapCopyOnWriteArrayList、阻塞队列。
  • 第 43 章 线程池——ThreadPoolExecutor 七参数、拒绝策略、ForkJoinPool
  • 第 44 章 CompletableFuture——异步编程的瑞士军刀。
  • 第 45 章 虚拟线程与结构化并发——Java 并发的未来。

每一章都是后一章的基石。打牢基础,后面的”高深”内容才不会成为空中楼阁。准备好了吗?让我们从下一章的”线程基础”开始,真正动手写并发代码。