1. 并发编程与任务调度导论

在开始写第一行并发代码之前,我们需要先把三个问题讲清楚:并发和并行到底差在哪里,任务调度在系统里扮演什么角色,以及这门课最终要交付一个什么样的项目。这三个问题决定了后面 29 章的技术选型和代码组织方式,如果一开始理解偏了,后面很容易写出"看起来能跑、实际上不可控"的程序。

一、并发与并行:不是同义词

并发(Concurrency)和并行(Parallelism)经常被混用,但它们的关注点完全不同。并发描述的是任务在时间上重叠推进的能力,它关心的是"多个任务如何被组织和切换";并行描述的是任务在同一时刻真正同时执行,它关心的是"有多少物理执行单元在干活"。

一个直观的判断方法:单核 CPU 上可以写出并发程序,但写不出并行程序。单核通过时间片轮转,让任务 A 执行一小段、切到任务 B、再切回来,宏观上像是"同时"在跑,但任意时刻只有一个任务占用 CPU。这就是并发。而并行必须有多核、多处理器或多台机器作为物理基础。

从工程角度看,两者带来的问题也不同。并发主要带来正确性问题:共享状态被交错修改导致数据竞争、死锁、活锁、可见性问题。并行主要带来效率与扩展性问题:任务划分是否均衡、加速比是否随核数增长、是否存在串行瓶颈。理解这个区分,能帮我们在排障时快速定位方向——程序结果偶尔出错,多半是并发正确性问题;程序结果正确但跑不快,多半是并行效率问题。

还有一个常被忽略的概念是异步。异步强调的是"发起操作后不阻塞等待结果,而是通过回调、Future 或事件通知来获取结果",它和并发、并行是三个不同维度的概念。一个异步程序可以是单线程的(如事件循环),也可以是并发的,甚至可以在多线程上并行执行。

二、任务调度在系统中做什么

任务调度是连接"任务"和"执行资源"的中间层。它要回答四个问题:谁来做(选择哪个线程、进程或节点)、什么时候做(立即、延迟、定时、按依赖触发)、按什么顺序做(优先级、FIFO、公平调度)、做不完怎么办(排队、拒绝、重试、降级)。

调度器存在于系统的多个层次。操作系统层面有 CPU 调度器,决定哪个线程占用处理器;语言运行时层面有线程池和协程调度器,决定任务分配到哪个工作线程;应用框架层面有任务队列和分布式调度器,决定任务在集群中的分发。我们这门课主要关注后两层,也就是应用级任务调度。

调度策略的选择直接影响系统行为。以线程池为例,当核心线程数已满、队列已满、最大线程数也已满时,拒绝策略就会生效。常见的四种策略——直接抛异常、由调用者线程执行、丢弃最旧任务、静默丢弃——适用于完全不同的业务场景。如果选错了,轻则任务丢失,重则整个服务被拖垮。这就是为什么调度不能只当成"配置几个参数",而需要理解其背后的模型。

并发 / 并行 / 异步 三维关系与调度位置 并发 Concurrency 任务在时间上重叠推进 单核即可实现(时间片轮转) 核心问题:数据竞争 / 死锁 关注:正确性 线程 / 协程 / 事件循环 并行 Parallelism 任务在同一时刻真正同时执行 需要多核 / 多机物理基础 核心问题:负载均衡 / 加速比 关注:效率与扩展性 多线程 / 多进程 / 分布式 异步 Async 发起后不阻塞等待结果 通过回调 / Future / 事件通知 可在单线程上实现 关注:响应性与吞吐 Promise / CompletableFuture 任务调度层:连接「任务」与「执行资源」 谁来做(线程/进程/节点) · 何时做(立即/延迟/定时/依赖触发) · 什么顺序(优先级/FIFO/公平) · 做不完怎么办(排队/拒绝/重试/降级) OS 调度器 → 运行时调度器(线程池/协程) → 应用/分布式调度器 线程池调度 核心/最大线程数 队列 + 拒绝策略 定时/延迟调度 ScheduledExecutor 时间轮 / 延迟队列 依赖调度 DAG / 有向无环图 拓扑排序触发 分布式调度 任务分发 / 心跳 故障转移

三、课程项目总览

本课程围绕一个贯穿始终的项目展开:构建一个可观测、可扩展的分布式任务调度引擎。它不是玩具 demo,而是包含真实工程要素的完整系统。项目分为五个递进阶段:

  • 阶段一:并发基础与线程模型。实现线程池、任务队列、拒绝策略,理解线程生命周期与状态转换。
  • 阶段二:同步与协作。用锁、信号量、条件变量、原子类解决共享状态问题,并实现生产者-消费者模型。
  • 阶段三:异步与 Future 编排。实现任务提交、结果获取、超时控制、任务组合(串行、并行、聚合)。
  • 阶段四:调度策略与 DAG 依赖。实现优先级调度、延迟调度、基于有向无环图的任务依赖触发。
  • 阶段五:分布式扩展与可观测性。加入任务分发、心跳检测、故障转移,并暴露指标用于监控。

每个阶段都会产出一个可运行的模块,最终组合成完整引擎。这样安排的好处是:每学一个知识点,都能立刻在项目里找到落点,而不是学完一堆 API 却不知道怎么用。

四、技术栈选型

技术栈的选择遵循三个原则:能直接暴露并发细节、生态成熟便于排障、贴近工业实践。基于这三点,本课程选用以下技术栈:

层次 技术选型 选择理由
语言与运行时 Java 17(LTS) JUC 并发包成熟,虚拟线程(Loom)可对比传统线程模型
并发原语 java.util.concurrent 线程池、阻塞队列、原子类、CompletableFuture 一应俱全
构建工具 Maven 3.9 依赖管理清晰,便于分模块组织项目
测试框架 JUnit 5 + Awaitility 支持异步断言,适合验证并发行为
可观测性 Micrometer + Prometheus 暴露线程池指标、任务延迟、队列深度
分布式协调 Redis(轻量方案) 用于任务分发与心跳,避免引入过重中间件
为什么不用 Go 或 Rust? 它们同样是优秀的并发语言,但 Java 的 JUC 包把并发问题"显式化"了——你需要亲手处理线程池、锁、队列,这恰恰是学习并发最好的方式。Go 的 goroutine 和 channel 把很多细节隐藏了,适合生产但不适合教学。Rust 的所有权模型学习曲线过陡,会分散对调度本身的注意力。

五、一个最小可运行示例

在进入正式项目之前,先用一段代码建立直觉。下面创建一个固定大小为 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 章)。每一章都会在项目里留下可运行的代码,建议边学边敲,不要只看不练。