臭名昭著aTrust隔离计划1.臭名昭著aTrust隔离计划2.VPN原理与aTrust隔离网络实践3.docker-easyconnect到底做了什么4.TUN(tunnel-隧道-虚拟网卡)模式
个人项目
personal(个人展示)GitHub Actions 自动部署个人展示项目
基于Quartz搭建的个人博客1.Quartz个人博客使用教程2.使用 rsync 增量部署 Quartz 博客3.使用 GitHub Actions 自动部署 Quartz 博客5.域名绑定6.CDN加速7.Google 搜索收录排查:为什么关键词搜得到别人,却搜不到我的网站?Obsidian、双向链接与知识图谱
架构设计
分布式分布式事务RPC原理
旅行
行程攻略青岛三日游行程攻略
前端
NginxNginx配置与反向代理入门
Nodenpm与npx的区别
数据库
mysql01.MySQL基础语法与约束02.MySQL数据库类型03.MySQL多表查询04.MySQL 索引05.MySQL 事务06.MySQL 隔离级别07.MySQL MVCC08.MySQL 锁09.EXPLAIN10.慢查询与 SQL 优化
mysql面试题01.索引问题02.事务与隔离级别03.锁机制04.存储引擎与性能优化05.用户权限、字符集与备份恢复
mysql实战01.EXPLAIN实战02.测试环境菜单误删排查与恢复03.MySQL IN 详解
网络
网络安全1.无线监听网卡苹果M系列虚拟机安装Kali Linux教程3.kali怎么使用RTL8812AU4.kali基础命令
网络基础进程之间怎样通过网络传递数据
网址常用网址
中间键
esElasticsearch 入门
mq
RabbitMQRabbitMQ高级RabbitMQ基础
1. MQ 基础2. MQ 核心角色与代码对应3. MQ Consumer Group 与消费模式4. SOFAMQ 账户变更代码实践5. MQ 可靠性问题6. 消息幂等
redis缓存击穿、穿透与雪崩Redis 分布式锁Redis 缓存问题Redis 数据结构
AI
使用说明
第三方插件&技能简介Archify使用与安装指南geekgeekRun使用OfferFlow使用指南Ponytail使用指南Skills Manager使用指南
CodexCodex CLI与IDE区别及使用指南Codex Hook单独配置与提交通知Codex MCP安装与使用指南Codex第三方插件安装与使用指南
Agent开发0-学习路线1.LLM、Token、上下文窗口与模型参数2.大模型 API、请求参数与响应结构3.Spring AI ChatClient4.Prompt、System Prompt、Prompt 模板6.结构化输出、JSON Schema7.SSE 流式响应8.会话 ID、聊天记录、Redis9.超时、重试、限流、降级10.*完成可运行聊天接口11.Function Calling、Tool Calling 原理12.用 Java 方法定义 Tool13.工具参数设计和校验14.实现订单查询工具15.实现物流查询工具16.实现库存查询工具17.多个工具自动选择18.工具异常、超时、重试
diy主机
主机零件学习1.CPU2.主板3.内存4.显卡5.硬盘6.散热7.风扇8.机箱热门型号价格表
捡垃圾diy-13600kf+5060diy-14600kf+5070
English高频词汇英文基础语法简单句子音标
GitGit常用命令Sublime Merge 使用指南
Java
工具库Caffeine本地缓存Reactor教程
工作实践如何使用@Async线程池使用与排查线上服务 CPU 使用率过高排查压测下RPC接口大量失败排查记录
基础1.HashMap2.ConcurrentHashMap3.HashSet4.final5.static6.volatile7.AtomicInteger8.String动态代理反射锁与线程同步线程注解
面试题1.HashMap 存储在方法内是线程安全的吗2.ArrayList参数传递与引用重新赋值3.== 和 equals 的区别、hashcode4.重载和重写的区别5.String、StringBuffer、StringBuilder区别及使用场景6.接口和抽象类7.ArrayList 与 LinkedList 的区别和扩容机制8.HashMap 和 Hashtable 的区别及底层实现9.Object 和 Objects 的区别10.ThreadLocal的原理和使用场景11.字节码是什么及作用12.Java类加载器有哪些13.双亲委派模型14.Java异常体系15.GC是什么16.GC如何判断对象可以被回收
面试总结0.HR面试模板1.面试一2.面试二3.面试三笔试
网络与 IO(黑马程序员-纯理论)01-网络编程基础02-IO模型与零拷贝03-BIO04-NIO基础与缓冲区05-NIO通道与选择器06-AIO
juc00-目录01-进程与线程02-线程同步与通信03-并发设计模式04-Java内存模型05-CAS与原子类06-ThreadLocal07-阻塞队列08-线程池使用与原理09-任务调度10-ForkJoin与享元模式11-AQS12-ReentrantLock13-读写锁14-并发协作工具15-ConcurrentHashMap16-CopyOnWrite与同步容器17-SkipListMap18-非阻塞队列
juc(工作常见)Java并发编程三个重点拆解
jvm1. JVM运行时数据区
Spring1.Spring IoC、DI与构造方法注入CGLIB、Byte Buddy 与 Spring AOPSpring 常用注解详解Spring Bean 生命周期
Spring MVC异常处理机制
linux常用命令
mac快捷键终端常用操作
python
基础01.Python是什么,为什么要安装Python02.Python项目中.venv是什么03.Python基础语法
基础实战sql替换
HTTPXHRMS员工详情接口调用(Python HTTPX)
windowswindows激活

一、💡 一句话理解

核心结论

线程是程序中实际执行代码的单位;线程池负责复用和管理线程;Future是提交任务后拿到的“结果凭证”。

学习顺序:先认识线程,再用线程池管理任务,最后用 Future 获取异步任务的结果。

线上已经出现线程 CPU 异常时,可继续阅读:../工作实践/线上服务 CPU 使用率过高排查。

二、🧭 理论:线程是什么

2.1 进程、线程和任务

名称白话解释例子
进程(Process)一个正在运行的程序启动后的 Java 程序。
线程(Thread)进程里真正执行代码的一条路线main 方法所在的主线程。
任务(Task)希望被执行的一段工作读取文件、计算统计结果。

一个 Java 程序至少有一个主线程。多个线程可以执行不同任务,并共享同一进程中的对象和数据。

一个 Java 进程
├─ 主线程:执行 main 方法
├─ 工作线程 A:读取文件
└─ 工作线程 B:计算数据

2.2 并发和并行

  • 并发(concurrency):多件事在一段时间内交替推进。
  • 并行(parallelism):多件事在不同 CPU 核心上真正同时执行。

实际开发中,重点是把彼此独立、耗时较长的任务交给不同线程,让主线程能继续做其他事情。

2.2.1 高并发和并行是什么关系

平时说的高并发,指的是同一时间段有很多请求或任务需要系统处理,或者有很多请求仍未完成;它不要求这些任务都真正同时占用 CPU。

概念关心的问题是否必须有多个 CPU 核心
高并发同时有多少事情需要处理、正在等待完成否
并发多件事能否交替推进否
并行此刻有多少件事真的同时在计算是

例如,一个网站同时有 1,000 个请求正在等待数据库返回,这就是高并发。即使服务器只有一个 CPU 核心,操作系统也能让多个线程轮流运行;某个线程等待网络或数据库时,CPU 可以切换去处理其他线程。

如果服务器有 4 个 CPU 核心,4 个计算量很大的任务可以分别在 4 个核心上同时运行,这一部分工作就是并行。多核带来的并行能力通常有助于处理 CPU 密集型任务,但它不是“高并发”的定义。

一句话区分

高并发关心“同时有多少事要处理”,并行关心“此刻有多少事真的一起在 CPU 上算”。

2.3 线程的常见状态

状态意思
NEW已创建线程对象,但还没调用 start()。
RUNNABLE可以运行;可能正在使用 CPU,也可能等待 CPU 调度。
BLOCKED正在等待进入被其他线程占用的 synchronized 锁。
WAITING无限期等待其他线程通知,例如 Object.wait()。
TIMED_WAITING有时间限制地等待,例如 Thread.sleep(1000)。
TERMINATEDrun() 方法已执行结束。

注意

RUNNABLE 不等于“此刻一定占用 CPU”。操作系统会在可运行的线程之间调度。

三、⚙️ 理论:如何创建和控制线程

3.1 用 start() 启动线程

run() 只是普通方法调用,仍在当前线程执行;只有 start() 才会启动新线程,再由新线程调用 run()。

public class ThreadStartDemo {
    public static void main(String[] args) {
        Thread worker = new Thread(() -> {
            System.out.println("工作线程:" + Thread.currentThread().getName());
        }, "file-worker");
 
        // 启动新线程。
        worker.start();
 
        // 这一行仍由 main 线程执行。
        System.out.println("主线程:" + Thread.currentThread().getName());
    }
}

两个输出的先后顺序不固定,因为两个线程由操作系统调度。

3.2 sleep、join 与中断

理解这三个方法,关键不是死记方法名,而是先看清楚:谁暂停、谁等待、谁收到通知。

方法谁受到影响生活场景一句话理解
Thread.sleep(...)执行这行代码的当前线程厨师自己休息一会儿我自己暂停
worker.join()调用 join() 的当前线程厨师等电饭煲把饭煮好我等别人结束
worker.interrupt()worker 线程店长通知电饭煲任务取消我通知别人考虑停止

3.2.1 sleep:当前线程自己暂停

假设 main 线程是一名厨师。厨师完成一道菜后,决定休息 3 秒:

public class SleepDemo {
    public static void main(String[] args) throws InterruptedException {
        System.out.println("开始休息");
 
        // 当前执行到这里的是 main 线程,
        // 所以暂停的是 main 线程。
        Thread.sleep(3_000);
 
        System.out.println("休息结束");
    }
}

输出过程如下:

开始休息
(等待大约 3 秒)
休息结束

sleep(3_000) 表示当前线程至少暂停大约 3 秒。时间到了以后,它只是重新具备运行资格,还要等待操作系统调度,不保证精确到 3 秒立即执行。

sleep 暂停的是当前线程

sleep 是静态方法。即使写成 worker.sleep(3_000),暂停的仍然是执行这行代码的当前线程,并不是 worker。因此始终建议写成 Thread.sleep(3_000)。

3.2.2 join:当前线程等待目标线程结束

现在让工作线程模拟电饭煲,让 main 线程模拟厨师。厨师可以先切菜,但必须等米饭煮好以后才能装盘:

public class JoinDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread riceCooker = new Thread(() -> {
            try {
                System.out.println("电饭煲:开始煮饭");
                Thread.sleep(3_000);
                System.out.println("电饭煲:饭煮好了");
            } catch (InterruptedException exception) {
                System.out.println("电饭煲:煮饭被取消");
            }
        }, "rice-cooker");
 
        riceCooker.start();
 
        System.out.println("main:先去切菜");
 
        // 当前执行到这里的是 main 线程。
        // main 在这里等待,直到 riceCooker 执行结束。
        riceCooker.join();
 
        System.out.println("main:米饭好了,开始装盘");
    }
}

“开始煮饭”和“先去切菜”的先后顺序不固定,但“开始装盘”一定出现在“饭煮好了”之后。

判断谁在等待

执行 riceCooker.join() 时,不是 riceCooker 等待,而是当前执行这行代码的线程等待 riceCooker 结束。在这个例子中,等待者是 main。

3.2.3 interrupt:向目标线程发出中断请求

假设电饭煲原计划煮饭 10 秒,但用户在 2 秒后取消了订单。main 线程可以调用 interrupt() 通知煮饭线程停止等待:

public class InterruptDemo {
    public static void main(String[] args) throws InterruptedException {
        Thread riceCooker = new Thread(() -> {
            try {
                System.out.println("电饭煲:开始煮饭");
 
                // riceCooker 线程在这里进入有时间限制的等待。
                Thread.sleep(10_000);
 
                System.out.println("电饭煲:饭煮好了");
            } catch (InterruptedException exception) {
                // sleep 期间收到 interrupt(),会抛出 InterruptedException。
                System.out.println("电饭煲:收到取消通知,停止煮饭");
                return;
            }
        }, "rice-cooker");
 
        riceCooker.start();
 
        // main 等待 2 秒,模拟用户稍后取消订单。
        Thread.sleep(2_000);
        System.out.println("main:用户取消订单");
 
        // 向 riceCooker 发出中断请求,不是强制杀死线程。
        riceCooker.interrupt();
 
        // main 等待 riceCooker 真正处理完中断并结束。
        riceCooker.join();
        System.out.println("main:订单处理结束");
    }
}

大致输出如下:

电饭煲:开始煮饭
main:用户取消订单
电饭煲:收到取消通知,停止煮饭
main:订单处理结束

完整过程是:

riceCooker 执行 sleep
        ↓
main 调用 riceCooker.interrupt()
        ↓
riceCooker 的 sleep 被打断
        ↓
抛出 InterruptedException,进入 catch
        ↓
riceCooker 清理并结束,main 的 join 才结束等待

中断不是“强制杀死线程”,而是一个协作信号:调用方负责发出请求,目标线程负责决定如何结束。如果线程正处于 sleep、join 或 wait 等可中断等待中,通常会抛出 InterruptedException。

3.2.4 为什么有时要恢复中断标记

线程因为 sleep、join 或 wait 被中断并抛出 InterruptedException 时,中断标记会被清除。如果当前方法不能把异常继续向上抛,但上层代码仍需要知道发生过中断,通常要恢复中断标记,然后尽快结束当前任务:

try {
    Thread.sleep(1_000);
} catch (InterruptedException exception) {
    // 把中断标记重新设置回去,告诉上层:当前线程收到过中断请求。
    Thread.currentThread().interrupt();
 
    // 停止当前任务,不要假装什么都没有发生后继续执行。
    return;
}

不要写一个空的 catch 后继续执行,否则外部明明要求任务停止,当前代码却把中断信号“吞掉”了。

记忆口诀

sleep:我自己暂停;join:我等别人结束;interrupt:我通知别人考虑停止。

3.3 wait、notify 与 synchronized

wait() 可以理解成:当前线程发现条件还不满足,于是释放锁,到旁边等待;其他线程把条件准备好后,再通知它回来。

3.3.1 它们属于 Thread 吗

wait()、notify() 和 notifyAll() 不是 Thread 类定义的方法,而是 Object 类的实例方法。每个 Java 对象都继承自 Object,因此每个对象都能作为线程等待和通知时使用的共享对象。

写法属于谁类型作用
wait()Object 类实例方法让当前线程释放该对象的锁并等待
notify()Object 类实例方法随机唤醒一个正在该对象上等待的线程
notifyAll()Object 类实例方法唤醒所有正在该对象上等待的线程
synchronizedJava 语言关键字让线程先获得对象锁,再执行临界区代码
sleep()Thread 类静态方法让当前线程暂停一段时间
join()Thread 类实例方法让当前线程等待目标线程结束
interrupt()Thread 类实例方法向目标线程发出中断请求

虽然语法上可以写 worker.wait(),但这只是因为 Thread 也继承了 Object.wait()。它表示“当前线程在 worker 这个对象的锁上等待”,并不是 Thread 专门提供的线程控制方法。普通业务代码通常不要把 Thread 对象当作锁,以免产生误解。

3.3.2 为什么必须配合 synchronized

假设共享对象是一间仓库:

生活场景Java 概念
仓库共享对象
仓库钥匙对象的 synchronized 锁
货物是否到达线程正在等待的条件
顾客交出钥匙并等待warehouse.wait()
配送员通知顾客warehouse.notifyAll()

调用 wait() 的线程必须先持有同一个对象的锁:

Object warehouse = new Object();
 
synchronized (warehouse) {
    // 当前线程释放 warehouse 的锁,
    // 并进入 warehouse 对象的等待集合。
    warehouse.wait();
}

另一个线程也必须通过同一个对象发出通知:

synchronized (warehouse) {
    // 唤醒所有正在 warehouse 对象上等待的线程。
    warehouse.notifyAll();
}

也就是说,下面三个位置必须使用同一个对象:

synchronized 使用 warehouse
wait 使用 warehouse
notifyAll 也使用 warehouse

如果拿着 lockA 的锁,却调用 lockB.wait(),运行时会抛出 IllegalMonitorStateException:

Object lockA = new Object();
Object lockB = new Object();
 
synchronized (lockA) {
    // 错误:当前线程没有持有 lockB 的锁。
    lockB.wait();
}

3.3.3 完整例子:顾客等待仓库到货

public class WaitDemo {
 
    // 两个线程共同使用的仓库对象,也是锁对象。
    private static final Object WAREHOUSE = new Object();
 
    // 两个线程共同访问的条件:货物是否已经到达。
    private static boolean goodsArrived = false;
 
    public static void main(String[] args) throws InterruptedException {
        Thread customer = new Thread(() -> {
            synchronized (WAREHOUSE) {
                System.out.println("顾客:进入仓库");
 
                // 必须使用 while 反复检查条件。
                // 被唤醒只表示条件可能变化,不保证货物一定已经到达。
                while (!goodsArrived) {
                    try {
                        System.out.println("顾客:货还没到,释放钥匙并等待");
 
                        // 当前线程进入等待状态,
                        // 同时释放 WAREHOUSE 对象的锁。
                        WAREHOUSE.wait();
 
                        // wait 返回前,当前线程必须重新获得 WAREHOUSE 的锁。
                        System.out.println("顾客:收到通知,重新检查货物");
                    } catch (InterruptedException exception) {
                        Thread.currentThread().interrupt();
                        System.out.println("顾客:等待被取消");
                        return;
                    }
                }
 
                System.out.println("顾客:货物已到,取走货物");
            }
        }, "customer");
 
        Thread deliveryWorker = new Thread(() -> {
            try {
                // 模拟配送需要 2 秒。
                Thread.sleep(2_000);
            } catch (InterruptedException exception) {
                Thread.currentThread().interrupt();
                return;
            }
 
            synchronized (WAREHOUSE) {
                System.out.println("配送员:把货物放进仓库");
 
                // 先修改顾客正在等待的条件。
                goodsArrived = true;
 
                // 再唤醒所有在 WAREHOUSE 对象上等待的线程。
                WAREHOUSE.notifyAll();
                System.out.println("配送员:通知顾客货物已到");
            }
        }, "delivery-worker");
 
        customer.start();
        deliveryWorker.start();
 
        customer.join();
        deliveryWorker.join();
        System.out.println("main:取货流程结束");
    }
}

大致输出如下:

顾客:进入仓库
顾客:货还没到,释放钥匙并等待
配送员:把货物放进仓库
配送员:通知顾客货物已到
顾客:收到通知,重新检查货物
顾客:货物已到,取走货物
main:取货流程结束

notifyAll() 只是把等待线程唤醒,不会把锁立即交给它们。配送员退出 synchronized 并释放锁后,顾客还要重新竞争并获得 WAREHOUSE 的锁,才能从 wait() 后面继续执行。

等待条件必须使用 while 检查,不能简单地使用 if:线程被唤醒只表示“条件可能发生了变化”,还可能出现虚假唤醒,或者条件又被其他线程改变。

3.3.4 wait 和 sleep 的区别

对比wait()sleep()
所属类ObjectThread
主要目的等待某个共享条件发生变化单纯暂停一段时间
是否必须在 synchronized 中是否
是否释放锁释放调用 wait() 的对象锁不释放已经持有的锁
如何恢复notify()、notifyAll()、中断、超时或虚假唤醒时间结束或被中断

一句话记忆

sleep 是拿着钥匙睡觉;wait 是交出钥匙,到旁边等通知。

四、🧭 理论:为什么需要线程池

4.1 线程池解决什么问题

频繁使用 new Thread(...).start() 有成本。任务突然增多时,线程数量会膨胀,占用内存并增加上下文切换。

线程池将“任务”和“线程”分开:

任务 1、2、3、4、5
        ↓ 提交
线程池:工作线程 + 等待队列
        ↓ 调度
有限数量的线程依次执行任务

它的作用是复用线程、限制同时执行的任务数、暂存等待任务,并在结束时统一回收资源。

4.2 ExecutorService、execute 与 submit

对象或方法用途返回值
ExecutorService管理线程和任务无固定结果
execute(Runnable)提交不关心返回值的任务void
submit(Runnable)提交无返回值任务,但可拿到状态Future<?>
submit(Callable<T>)提交有返回值的任务Future<T>

Runnable 没有返回值;Callable<T> 可以返回 T,也可以抛出异常。

4.3 关闭线程池

  • shutdown():不再接收新任务,已提交任务继续完成。
  • shutdownNow():尝试中断正在执行的任务,并返回尚未开始的任务;它不能保证任务一定立即停止。

4.4 ThreadPoolExecutor 的七个参数

Executors.newFixedThreadPool(2) 写起来简单,但它把许多配置隐藏了。需要明确控制线程数、队列和拒绝方式时,直接创建 ThreadPoolExecutor:

new ThreadPoolExecutor(
        2,                              // corePoolSize:核心线程数
        4,                              // maximumPoolSize:最大线程数
        30,                             // keepAliveTime:非核心线程的空闲存活时间
        TimeUnit.SECONDS,               // 时间单位
        new ArrayBlockingQueue<>(10),   // workQueue:最多等待 10 个任务
        threadFactory,                  // ThreadFactory:创建线程的规则
        new ThreadPoolExecutor.AbortPolicy() // 拒绝策略
);
参数白话解释例子中的含义
corePoolSize常驻员工数通常保留 2 个工作线程。
maximumPoolSize最多允许多少员工繁忙时最多扩到 4 个线程。
keepAliveTime临时员工空闲多久后离开超过核心数的空闲线程 30 秒后可回收。
unit上一项的时间单位秒。
workQueue员工忙时,任务在哪里等候最多排队 10 个任务。
threadFactory如何创建线程统一命名,便于日志排查。
handler线程和队列都满时怎么办抛出异常,明确告诉调用方任务未被接收。

4.5 新任务进入线程池的顺序

以“核心线程数 2、最大线程数 4、队列容量 10”为例,每提交一个任务,线程池按下面的顺序处理:

1. 当前工作线程少于 2 个 → 创建核心线程执行任务
2. 核心线程已满 → 任务先进入队列等待
3. 队列也满,且线程数少于 4 个 → 创建非核心线程执行任务
4. 队列满,线程数也达到 4 个 → 执行拒绝策略

最容易混淆的点

不是先创建到最大线程数,再排队。 默认策略是:先补足核心线程,然后优先入队;只有队列满了,才会继续创建非核心线程。

4.6 工作队列怎么选

队列特点适用理解
ArrayBlockingQueue<>(n)有固定容量最容易控制积压量,初学时优先理解它。
LinkedBlockingQueue<>()默认可视为无界队列任务可能一直堆积;此时 maximumPoolSize 基本不会生效。
SynchronousQueue不存放任务,必须直接交给空闲线程往往需要较大的最大线程数,线程数可能快速增长。

队列不是越大越好。无界队列能暂时少报错,但任务积压会越来越多,等待时间和内存占用也会增长。有限队列能让系统更早暴露“处理不过来”的事实。

4.7 四种内置拒绝策略

当线程池已关闭,或“最大线程数和队列容量都到上限”时,新任务会被拒绝。

策略行为适合什么情况
AbortPolicy抛出 RejectedExecutionException默认选择;调用方必须明确处理失败。
CallerRunsPolicy提交任务的线程自己执行任务以降低提交速度为代价,形成简单的反压。
DiscardPolicy直接丢弃新任务只有任务丢失完全无影响时才考虑。
DiscardOldestPolicy丢弃队列中等待最久的任务,再尝试提交新任务很少适合普通业务,容易丢掉用户先提交的工作。

4.8 给线程命名

默认线程名如 pool-1-thread-1 不容易定位。ThreadFactory 可以统一命名:

import java.util.concurrent.ThreadFactory;
import java.util.concurrent.atomic.AtomicInteger;
 
AtomicInteger sequence = new AtomicInteger(1);
ThreadFactory threadFactory = task -> {
    // 每创建一个线程,生成一个清晰且不重复的名称。
    Thread thread = new Thread(task, "report-worker-" + sequence.getAndIncrement());
    return thread;
};

发生异常或查看日志时,report-worker-1 比默认名称更容易判断这条线程属于哪个业务线程池。

4.9 一个有界线程池的完整例子

下面的线程池最多同时执行 4 个任务,最多等待 10 个任务;容量耗尽时直接抛出异常。它适合理解配置含义,实际参数应按任务类型和压测结果调整。

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicInteger;
 
public class CustomThreadPoolDemo {
    public static void main(String[] args) {
        AtomicInteger sequence = new AtomicInteger(1);
        ThreadFactory factory = task ->
                new Thread(task, "report-worker-" + sequence.getAndIncrement());
 
        ExecutorService pool = new ThreadPoolExecutor(
                2, 4,
                30, TimeUnit.SECONDS,
                new ArrayBlockingQueue<>(10),
                factory,
                new ThreadPoolExecutor.AbortPolicy()
        );
 
        try {
            for (int taskNumber = 1; taskNumber <= 3; taskNumber++) {
                final int currentTask = taskNumber; // Lambda 中使用的局部变量必须不可变。
                pool.execute(() -> System.out.println(
                        Thread.currentThread().getName() + " 执行任务 " + currentTask));
            }
        } finally {
            // 示例结束后关闭线程池,避免工作线程一直存活。
            pool.shutdown();
        }
    }
}

运行后可以看到带有 report-worker- 前缀的线程名。三个任务中,前两个会创建核心线程执行,第三个通常进入队列等待。

五、🚀 实践:线程池与 Future

5.1 前置准备

以下示例只需要 JDK 17 或更高版本。将代码保存为 ThreadPoolFutureDemo.java,然后执行:

javac ThreadPoolFutureDemo.java
java ThreadPoolFutureDemo

5.2 可以拿来干什么

5.2.1 后台执行不需要结果的任务

例如记录操作日志。这个任务只需要完成动作,不需要把结果交回调用方,因此使用 Runnable。调用方只负责提交,任务由线程池中的工作线程执行:

ExecutorService pool = Executors.newFixedThreadPool(2);
 
// Runnable 的 run() 没有返回值。
Runnable logTask = () -> {
    // 这里是独立执行的后台任务。
    System.out.println("记录操作日志");
};
 
// execute 只负责提交 Runnable,不提供任务执行状态或结果。
pool.execute(logTask);
 
// 不再接收新任务;已提交的任务仍会执行。
pool.shutdown();

常见场景:写操作日志、发送通知、刷新本地缓存、清理临时文件等“做完即可”的工作。

如果仍希望知道 Runnable 是否执行结束,也可以写 Future<?> future = pool.submit(logTask);任务成功结束后 future.get() 的结果为 null。

5.2.2 同时处理两个独立任务,再汇总结果

这里使用的是 Callable<Integer>:两个任务都要计算并返回一个整数。submit(Callable<T>) 立即返回 Future<T>,get() 在结果真正需要使用时再等待。

前置:pool 是已创建的 ExecutorService。如果任务不需要返回值,才使用 Runnable 并调用 execute(...) 或 submit(...)。

// Lambda 的返回值使其匹配 Callable<Integer>,而不是 Runnable。
Callable<Integer> firstTask = () -> 10 + 20;
Callable<Integer> secondTask = () -> 30 + 40;
 
Future<Integer> first = pool.submit(firstTask);
Future<Integer> second = pool.submit(secondTask);
 
// get() 会等待对应任务完成,并取回返回值。
int total = first.get() + second.get();
System.out.println(total); // 100

常见场景:计算统计值、读取文件内容、查询数据库、调用第三方接口等“做完后还要使用结果”的工作。Callable 还可以抛出受检异常,因此适合可能出现 I/O 异常的任务。

5.2.3 Runnable 和 Callable 怎么选

你的需求选择原因
只要任务执行,不关心返回内容Runnablerun() 没有返回值,写法更直接。
要拿到计算、读取或查询的结果Callable<T>call() 可以返回 T。
任务可能抛出受检异常Callable<T>call() 可以声明 throws Exception。
只想知道无返回值任务是否完成Runnable + submit通过 Future<?> 查询状态或等待完成,正常结果为 null。

5.3 完整实践:计算任务结果并处理异常

下面的程序演示创建固定线程池、提交两个 Callable、通过 Future 获取结果,以及关闭线程池。

import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
 
public class ThreadPoolFutureDemo {
    public static void main(String[] args) {
        // 最多两个任务同时执行。
        ExecutorService pool = Executors.newFixedThreadPool(2);
 
        try {
            Callable<Integer> sumTask = () -> {
                Thread.sleep(500); // 模拟耗时计算。
                return 1 + 2 + 3;
            };
            Callable<Integer> productTask = () -> {
                Thread.sleep(300); // 模拟另一项耗时计算。
                return 4 * 5;
            };
 
            // 提交后立即得到 Future;任务可能正在执行,也可能在队列中等待。
            Future<Integer> sumFuture = pool.submit(sumTask);
            Future<Integer> productFuture = pool.submit(productTask);
 
            // 最多等待 2 秒;正常完成时,返回 Callable 的结果。
            int sum = sumFuture.get(2, TimeUnit.SECONDS);
            int product = productFuture.get(2, TimeUnit.SECONDS);
            System.out.println("sum = " + sum);
            System.out.println("product = " + product);
        } catch (TimeoutException exception) {
            System.err.println("任务超时");
        } catch (InterruptedException exception) {
            // 等待结果时被中断,恢复中断标记。
            Thread.currentThread().interrupt();
            System.err.println("等待结果时被中断");
        } catch (ExecutionException exception) {
            // 任务内部异常会被包装;getCause() 是原始异常。
            System.err.println("任务执行失败:" + exception.getCause());
        } finally {
            pool.shutdown();
            try {
                if (!pool.awaitTermination(3, TimeUnit.SECONDS)) {
                    pool.shutdownNow();
                }
            } catch (InterruptedException exception) {
                pool.shutdownNow();
                Thread.currentThread().interrupt();
            }
        }
    }
}

预期输出:

sum = 6
product = 20

5.4 Future.get() 的结果

情况get() 的表现应对方式
任务正常完成返回任务结果使用结果。
任务内部抛异常抛出 ExecutionException查看 getCause()。
等待被中断抛出 InterruptedException恢复中断标记或继续向上抛出。
指定等待时间已到抛出 TimeoutException处理超时;不再需要任务时可尝试 cancel(true)。

get() 会阻塞

调用 future.get() 的线程会等待结果。先提交多个独立任务,再在需要结果时调用 get(),它们才有机会并发执行。

六、📌 共享数据与线程安全

本章先说明线程为什么会共享数据。关于临界区、synchronized、ReentrantLock 以及 Java 锁为什么不能跨 JVM,可以继续阅读 锁与线程同步。

6.1 同一进程中的线程共享什么

同一个 Java 进程内的线程可以访问同一份堆内存,因此会共享对象、单例和静态变量;这也是多线程协作方便、但需要注意线程安全的原因。

是否共享内容例子
共享堆内存中的对象new User()、集合、缓存。
共享静态变量和类相关资源static 字段、常量。
共享进程持有的资源已打开的文件、网络连接。
不共享线程栈局部变量、方法调用链。
不共享程序计数器每个线程各自执行到哪一行代码。
不共享ThreadLocal 数据绑定到当前线程的独立数据。

例如 Spring 应用中的 UserService 通常是单例对象,多个请求线程会共同访问它。如果把可变的请求数据放进它的成员变量,就可能被其他请求线程同时读写。

6.2 RPC 服务之间不共享内存

RPC 的两端通常运行在不同的 Java 进程,甚至不同的机器上,所以服务 A 与服务 B 的线程和内存彼此独立。请求参数和返回值需要序列化后通过网络传输。

服务 A:线程与内存  ── 序列化 + 网络 ──>  服务 B:线程与内存

因此,服务 A 中的静态变量、缓存或对象,服务 B 都无法直接访问。每个服务内部的线程仍共享本服务自己的内存。

6.3 为什么 count++ 不安全

count++ 包含读取、加一、写回三步。两个线程交错执行时,可能都读取到相同旧值,导致一次增加丢失。

初始 count = 0
线程 A 读取 0        线程 B 读取 0
线程 A 写回 1        线程 B 写回 1
最终是 1,而不是 2

6.4 常用处理方式

场景常用工具
简单计数、递增递减AtomicInteger
一段代码必须一次只允许一个线程执行synchronized
更灵活的加锁Lock
线程间传递任务BlockingQueue
import java.util.concurrent.atomic.AtomicInteger;
 
AtomicInteger count = new AtomicInteger(0);
count.incrementAndGet(); // 原子地加一并返回新值。

七、🧾 快速回顾

  • 线程是执行代码的单位;任务是要执行的工作。
  • 用 start() 启动新线程,不要把调用 run() 当作启动线程。
  • 线程池复用线程并限制并发,常用接口是 ExecutorService。
  • submit 返回 Future;get 取结果,但会让当前线程等待。
  • InterruptedException 不要直接吞掉,通常应恢复中断标记或继续向上抛出。
  • 多线程共享可变数据时,要使用合适的线程安全工具。
  • 延伸阅读:Reactor教程 介绍了响应式数据流与线程调度。

一句话记忆:线程负责执行,线程池负责安排,Future 负责取回结果。

八、🔗 官方资料