并发基础

欢迎来到第七阶段——并发编程。这是整个 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 里的 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里程碑核心能力
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、读写锁、StampedLock、Semaphore、CountDownLatch 等。
  • 第 41 章 原子类与 CAS——无锁编程的基石。
  • 第 42 章 并发集合——ConcurrentHashMap、CopyOnWriteArrayList、阻塞队列。
  • 第 43 章 线程池——ThreadPoolExecutor 七参数、拒绝策略、ForkJoinPool。
  • 第 44 章 CompletableFuture——异步编程的瑞士军刀。
  • 第 45 章 虚拟线程与结构化并发——Java 并发的未来。

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