Jakob Jenkov 2024-02-28
Java 并发(Concurrency)是一个涵盖多线程(Multithreading)、并发和并行处理的术语。它包括 Java 平台上的并发工具、常见问题及其解决方案。本 Java 并发教程系列涵盖了多线程的核心概念、并发结构、并发问题,以及 Java 中多线程相关的成本与收益。
Java 的并发和多线程特性一直在不断演进。最新的功能包括 Java 虚拟线程(Virtual Threads) 和 结构化并发(Structured Concurrency)。因此,请定期回访本页面以获取最新内容。
什么是多线程?
多线程意味着在同一个应用程序中存在多个执行线程。你可以把线程想象成一个独立的 CPU 在执行你的程序。因此,一个多线程应用程序就像是拥有多个 CPU 同时执行代码的不同部分。

但线程并不等同于 CPU。通常,单个 CPU 会在多个线程之间共享其执行时间,轮流为每个线程分配一定的时间片。此外,也可以让不同线程由不同的 CPU 执行。

为什么要使用多线程?
使用多线程有多种原因,最常见的包括:
- 更好地利用单个 CPU
- 更好地利用多个 CPU 或 CPU 核心
- 提升用户交互的响应性体验
- 提升资源分配的公平性
下面将逐一详细说明。
更好地利用单个 CPU
一个常见原因是更高效地利用计算机资源。例如,当一个线程正在等待网络请求的响应时,另一个线程可以利用 CPU 执行其他任务。此外,如果计算机拥有多个 CPU 或多核 CPU,多线程还能帮助应用程序充分利用这些额外的计算核心。
更好地利用多个 CPU 或 CPU 核心
如果计算机包含多个 CPU,或 CPU 拥有多个执行核心,那么你需要使用多个线程才能让应用程序充分利用所有 CPU 或核心。单个线程最多只能使用一个 CPU,甚至有时还不能完全占满一个 CPU。
提升用户交互的响应性体验
另一个使用多线程的原因是为了提供更好的用户体验。例如,在图形用户界面(GUI)中点击一个按钮触发网络请求时,如果该请求由 GUI 线程执行,用户可能会感觉界面“卡住”了,因为 GUI 线程在等待响应。而如果将请求交给后台线程处理,GUI 线程就能继续响应用户的其他操作。
提升资源分配的公平性
第四个原因是更公平地在多个用户之间共享计算机资源。例如,设想一个服务器只用一个线程处理所有客户端请求。如果某个客户端发送了一个耗时很长的请求,其他所有客户端的请求都必须排队等待。而如果每个请求都由独立的线程处理,就不会出现某个任务独占 CPU 的情况。
多线程 vs. 多任务
在早期,计算机只有一个 CPU,一次只能运行一个程序。大多数小型计算机没有足够的性能来同时运行多个程序,因此也没有尝试这么做(不过大型机系统很早就支持多任务了)。
多任务(Multitasking)
后来出现了多任务处理,即计算机可以“同时”运行多个程序(也称为任务或进程)。实际上并非真正的同时执行,而是操作系统在多个程序之间快速切换,每个程序轮流获得一小段时间片来运行。
多任务带来了新的挑战:程序不能再假设自己独占 CPU、内存或其他系统资源。一个“良好公民”程序应当及时释放不再使用的资源,以便其他程序使用。
多线程(Multithreading)
再后来出现了多线程,即在同一个程序内部可以有多个执行线程。你可以把每个线程看作一个独立的 CPU 在执行程序。当多个线程执行同一程序时,就相当于多个 CPU 在程序内部并行工作。
多线程很难
多线程确实能显著提升某些类型程序的性能,但它比多任务更具挑战性。因为多个线程运行在同一个程序中,会同时读写相同的内存区域,这可能导致单线程程序中不会出现的错误。有些错误在单 CPU 机器上甚至不会显现,因为两个线程实际上并未“真正同时”执行。但现代计算机普遍配备多核 CPU,甚至多个 CPU,这意味着不同线程可以真正并行执行。

- 如果一个线程正在读取某块内存,而另一个线程同时在写入,第一个线程最终会读到什么值?是旧值?新值?还是两者的混合?
- 如果两个线程同时写入同一内存位置,最终会留下哪个值?第一个线程写的?第二个线程写的?还是某种混合结果?
如果不采取适当措施,上述任何结果都可能发生,且行为不可预测,每次运行结果可能不同。因此,开发者必须掌握如何正确控制线程对共享资源(如内存、文件、数据库等)的访问。这也是本 Java 并发教程要解决的核心问题之一。
Java 中的多线程与并发
Java 是最早让多线程对开发者易于使用的编程语言之一。从诞生之初,Java 就内置了多线程能力。因此,Java 开发者经常面临上述并发问题。这正是我撰写本 Java 并发教程的原因——既作为自己的笔记,也希望对其他 Java 开发者有所帮助。
本教程主要关注 Java 多线程,但其中一些问题也与多任务处理和分布式系统中的问题类似,因此也会涉及相关内容。这也是为什么使用“并发”而非仅“多线程”作为标题的原因。
并发模型
最初的 Java 并发模型假设同一应用内的多个线程会共享对象。这种模型通常被称为 “共享状态并发模型”(shared state concurrency model)。许多并发语言结构和工具都是为支持该模型而设计的。
然而,自第一本 Java 并发书籍问世、乃至 Java 5 并发工具发布以来,并发架构与设计领域已发生巨大变化。
共享状态模型容易引发大量难以优雅解决的并发问题。因此,一种名为 “无共享”(shared nothing) 或 “分离状态”(separate state) 的替代模型逐渐流行起来。在这种模型中,线程之间不共享任何对象或数据,从而避免了共享状态模型中的并发访问问题。
近年来,出现了许多基于异步“分离状态”模型的新平台和工具包,例如 Netty、Vert.x、Play/Akka 和 Qbit。同时,新的非阻塞并发算法被提出,非阻塞工具如 LMax Disruptor 也被引入。此外,Java 7 引入了 Fork/Join 框架,Java 8 引入了集合流(Stream)API,进一步支持函数式并行编程。
鉴于这些新发展,是时候更新本 Java 并发教程了。因此,本教程目前仍在持续编写中,新内容将随时间陆续发布。
Java 并发学习指南
如果你是 Java 并发的新手,建议按照以下学习计划进行。你也可以在页面左侧菜单中找到所有主题的链接。
并发与多线程通用理论
Java 并发基础
- 创建与启动 Java 线程
- 竞态条件与临界区
- 线程安全与共享资源
- 线程安全与不可变性
- Java 内存模型
- Java Happens-Before 保证
- Java synchronized 块
- Java volatile 关键字
- Java 并发中的 CPU 缓存一致性
- Java ThreadLocal
- Java 线程通信(Thread Signaling)
Java 并发中的典型问题
- 死锁(Deadlock)
- 死锁预防
- 饥饿与公平性
- 嵌套监视器锁死
- 条件竞争(Slipped Conditions)
- 伪共享(False Sharing)
- 线程拥塞(Thread Congestion)
应对上述问题的 Java 并发结构
- Java 中的锁(Locks)
- Java 读写锁(Read/Write Locks)
- 重入锁死(Reentrance Lockout)
- 信号量(Semaphores)
- 阻塞队列(Blocking Queues)
- 线程池(Thread Pools)
- 比较并交换(Compare and Swap)