1. 并发编程与任务调度导论
在开始写第一行并发代码之前,我们需要先把三个问题讲清楚:并发和并行到底差在哪里,任务调度在系统里扮演什么角色,以及这门课最终要交付一个什么样的项目。这三个问题决定了后面 29 章的技术选型和代码组织方式,如果一开始理解偏了,后面很容易写出"看起来能跑、实际上不可控"的程序。
一、并发与并行:不是同义词
并发(Concurrency)和并行(Parallelism)经常被混用,但它们的关注点完全不同。并发描述的是任务在时间上重叠推进的能力,它关心的是"多个任务如何被组织和切换";并行描述的是任务在同一时刻真正同时执行,它关心的是"有多少物理执行单元在干活"。
一个直观的判断方法:单核 CPU 上可以写出并发程序,但写不出并行程序。单核通过时间片轮转,让任务 A 执行一小段、切到任务 B、再切回来,宏观上像是"同时"在跑,但任意时刻只有一个任务占用 CPU。这就是并发。而并行必须有多核、多处理器或多台机器作为物理基础。
从工程角度看,两者带来的问题也不同。并发主要带来正确性问题:共享状态被交错修改导致数据竞争、死锁、活锁、可见性问题。并行主要带来效率与扩展性问题:任务划分是否均衡、加速比是否随核数增长、是否存在串行瓶颈。理解这个区分,能帮我们在排障时快速定位方向——程序结果偶尔出错,多半是并发正确性问题;程序结果正确但跑不快,多半是并行效率问题。
还有一个常被忽略的概念是异步。异步强调的是"发起操作后不阻塞等待结果,而是通过回调、Future 或事件通知来获取结果",它和并发、并行是三个不同维度的概念。一个异步程序可以是单线程的(如事件循环),也可以是并发的,甚至可以在多线程上并行执行。
二、任务调度在系统中做什么
任务调度是连接"任务"和"执行资源"的中间层。它要回答四个问题:谁来做(选择哪个线程、进程或节点)、什么时候做(立即、延迟、定时、按依赖触发)、按什么顺序做(优先级、FIFO、公平调度)、做不完怎么办(排队、拒绝、重试、降级)。
调度器存在于系统的多个层次。操作系统层面有 CPU 调度器,决定哪个线程占用处理器;语言运行时层面有线程池和协程调度器,决定任务分配到哪个工作线程;应用框架层面有任务队列和分布式调度器,决定任务在集群中的分发。我们这门课主要关注后两层,也就是应用级任务调度。
调度策略的选择直接影响系统行为。以线程池为例,当核心线程数已满、队列已满、最大线程数也已满时,拒绝策略就会生效。常见的四种策略——直接抛异常、由调用者线程执行、丢弃最旧任务、静默丢弃——适用于完全不同的业务场景。如果选错了,轻则任务丢失,重则整个服务被拖垮。这就是为什么调度不能只当成"配置几个参数",而需要理解其背后的模型。
三、课程项目总览
本课程围绕一个贯穿始终的项目展开:构建一个可观测、可扩展的分布式任务调度引擎。它不是玩具 demo,而是包含真实工程要素的完整系统。项目分为五个递进阶段:
- 阶段一:并发基础与线程模型。实现线程池、任务队列、拒绝策略,理解线程生命周期与状态转换。
- 阶段二:同步与协作。用锁、信号量、条件变量、原子类解决共享状态问题,并实现生产者-消费者模型。
- 阶段三:异步与 Future 编排。实现任务提交、结果获取、超时控制、任务组合(串行、并行、聚合)。
- 阶段四:调度策略与 DAG 依赖。实现优先级调度、延迟调度、基于有向无环图的任务依赖触发。
- 阶段五:分布式扩展与可观测性。加入任务分发、心跳检测、故障转移,并暴露指标用于监控。
每个阶段都会产出一个可运行的模块,最终组合成完整引擎。这样安排的好处是:每学一个知识点,都能立刻在项目里找到落点,而不是学完一堆 API 却不知道怎么用。
四、技术栈选型
技术栈的选择遵循三个原则:能直接暴露并发细节、生态成熟便于排障、贴近工业实践。基于这三点,本课程选用以下技术栈:
| 层次 | 技术选型 | 选择理由 |
|---|---|---|
| 语言与运行时 | Java 17(LTS) | JUC 并发包成熟,虚拟线程(Loom)可对比传统线程模型 |
| 并发原语 | java.util.concurrent | 线程池、阻塞队列、原子类、CompletableFuture 一应俱全 |
| 构建工具 | Maven 3.9 | 依赖管理清晰,便于分模块组织项目 |
| 测试框架 | JUnit 5 + Awaitility | 支持异步断言,适合验证并发行为 |
| 可观测性 | Micrometer + Prometheus | 暴露线程池指标、任务延迟、队列深度 |
| 分布式协调 | Redis(轻量方案) | 用于任务分发与心跳,避免引入过重中间件 |
五、一个最小可运行示例
在进入正式项目之前,先用一段代码建立直觉。下面创建一个固定大小为 2 的线程池,提交 5 个任务,观察它们的执行顺序:
import java.util.concurrent.*;
public class FirstPool {
public static void main(String[] args) throws Exception {
ExecutorService pool = Executors.newFixedThreadPool(2);
for (int i = 1; i <= 5; i++) {
final int id = i;
pool.submit(() -> {
System.out.printf("任务%d 由 %s 执行%n",
id, Thread.currentThread().getName());
try { Thread.sleep(200); } catch (InterruptedException e) {}
});
}
pool.shutdown();
pool.awaitTermination(5, TimeUnit.SECONDS);
}
}
运行后你会看到类似输出(线程名可能不同):
任务1 由 pool-1-thread-1 执行
任务2 由 pool-1-thread-2 执行
任务3 由 pool-1-thread-1 执行
任务4 由 pool-1-thread-2 执行
任务5 由 pool-1-thread-1 执行
这里有几个值得注意的现象。第一,只有两个线程名反复出现,说明线程被复用了,这正是线程池存在的意义——避免频繁创建销毁线程的开销。第二,任务 3 在任务 1 之后由同一个线程执行,说明任务在队列中排队等待。第三,输出顺序不保证与提交顺序完全一致,因为两个线程是并发取任务的。这个"顺序不确定"就是并发编程最典型的特征,也是后面所有同步机制要解决的问题。
submit 之后任务就立刻开始执行。实际上,如果核心线程都在忙,任务会先进入队列等待。理解"提交"和"执行"之间的这段距离,是理解调度的第一步。
六、本章小结与学习路径
本章建立了三个核心认知:并发是任务重叠推进的能力,并行是物理同时执行,异步是不阻塞等待结果;调度层负责把任务映射到执行资源,决定谁做、何时做、什么顺序、失败怎么办;本课程将用 Java 17 + JUC 构建一个五阶段递进的分布式任务调度引擎。
接下来的学习路径是:先掌握线程与线程池(第 2-5 章),再深入锁与同步(第 6-10 章),然后进入异步编排(第 11-15 章),接着实现调度策略与 DAG(第 16-22 章),最后完成分布式扩展与可观测性(第 23-30 章)。每一章都会在项目里留下可运行的代码,建议边学边敲,不要只看不练。