Java高并发编程

欢迎你来读这篇博客,这篇博客主要是关于Java高并发编程
其中包括了关于我的见解和收集的知识分享。

序言

Java 高并发编程不是一上来就背 synchronizedvolatileReentrantLockThreadPoolExecutor 参数。真正要理解并发,先要搞清楚几个底层概念:

程序是什么?进程是什么?线程是什么?虚拟线程又是什么?

串行、并行、并发、同步、异步到底有什么区别?

为什么多线程会出现线程安全问题?为什么会有原子性、可见性、有序性问题?

JMM 到底解决了什么问题?

线程有几种状态?为什么有人说 5 种,有人说 6 种,有人说 7 种?

本文按 Java 高并发学习路径,把基础概念、执行流程、线程状态、线程创建方式和 JUC 入门体系串起来讲一遍。

一、程序、进程、线程、虚拟线程

1. 程序

程序是静态的代码文件。

比如:

1
2
3
4
5
public class Main {
public static void main(String[] args) {
System.out.println("Hello Java");
}
}

这段代码保存在磁盘上,它只是一个 .java.class 文件,还没有真正运行。

可以把程序理解成“菜谱”。菜谱本身不会做饭,它只是描述了怎么做。

2. 进程

进程是程序运行起来之后的实例。

当我们执行:

1
java Main

操作系统会启动一个 Java 进程。

进程拥有独立的系统资源,比如:

资源 说明
内存空间 每个进程有自己的内存地址空间
文件句柄 打开的文件、网络连接
代码段 程序指令
数据段 全局变量、静态数据
堆空间 Java 对象主要存放的位置
线程资源 一个进程至少有一个线程

进程之间默认是隔离的。一个进程崩溃,通常不会直接影响另一个进程。

3. 线程

线程是进程中的执行单元。

一个 Java 程序启动后,JVM 会创建主线程执行 main 方法。

1
2
3
public static void main(String[] args) {
System.out.println(Thread.currentThread().getName());
}

输出通常是:

1
main

说明 main 方法是由 main 线程执行的。

线程共享同一个进程的堆内存、方法区等资源,但每个线程有自己的:

私有区域 说明
程序计数器 当前线程执行到哪条字节码指令
虚拟机栈 方法调用栈、局部变量表
本地方法栈 native 方法调用栈

线程之间共享堆内存,所以多个线程同时访问同一个对象时,就可能产生线程安全问题。

4. 平台线程

Java 传统线程通常也叫平台线程

平台线程本质上是 Java 对操作系统线程的封装:

1
2
3
4
Thread thread = new Thread(() -> {
System.out.println("hello");
});
thread.start();

每创建一个平台线程,底层通常会对应一个操作系统线程。

平台线程的特点:

特点 说明
成本较高 创建、销毁、上下文切换都比较重
数量有限 不能无限创建,创建太多会耗尽内存或调度资源
适合 CPU 密集型和一般并发场景 比如计算任务、业务线程池
依赖操作系统调度 线程切换由 OS 调度器控制

5. 虚拟线程

虚拟线程是 JDK 管理的轻量级线程。

从 Java 21 开始,虚拟线程成为正式特性。

创建虚拟线程:

1
2
3
Thread.startVirtualThread(() -> {
System.out.println("hello virtual thread");
});

或者:

1
2
3
4
5
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
executor.submit(() -> {
System.out.println("task running in virtual thread");
});
}

虚拟线程也是 java.lang.Thread 的实例,但它不是和操作系统线程一一绑定的。

简单理解:

类型 类比
平台线程 一个任务长期占一个工人
虚拟线程 很多任务被 JVM 调度到少量工人身上执行

虚拟线程适合:

场景 是否适合
大量 I/O 阻塞任务 非常适合
大量 HTTP 请求 适合
大量数据库查询 适合,但要注意连接池限制
CPU 密集型计算 不适合提升计算速度
替代所有线程池 不建议无脑替代

虚拟线程最大的价值不是让单个任务跑得更快,而是让阻塞式代码拥有更高吞吐。

一句话总结:

平台线程是操作系统级别的重资源线程;虚拟线程是 JVM 管理的轻量级线程,适合高并发 I/O 阻塞场景。

6. 程序、进程、线程、虚拟线程对比

概念 本质 是否运行 资源归属 说明
程序 静态代码 磁盘文件 .java.class、jar
进程 程序运行实例 操作系统分配 一个 JVM 通常是一个进程
线程 进程内执行单元 共享进程资源 Java 代码真正执行的单位
平台线程 OS 线程封装 操作系统调度 成本高,数量有限
虚拟线程 JVM 轻量线程 JVM 调度 成本低,适合大量阻塞任务

二、串行、并行、并发、同步、异步

1. 串行

串行是一个任务执行完,再执行下一个任务。

1
2
3
task1();
task2();
task3();

执行顺序:

1
task1 -> task2 -> task3

特点:

优点 缺点
简单、稳定、容易理解 效率低,不能充分利用多核 CPU

2. 并行

并行是多个任务在同一时刻真正同时执行。

前提通常是多核 CPU。

例如:

1
2
3
CPU 核心 1 执行 task1
CPU 核心 2 执行 task2
CPU 核心 3 执行 task3

并行强调的是同一时刻同时执行

3. 并发

并发是多个任务在同一时间段内交替推进。

并发不一定是真正同时执行。

单核 CPU 也可以并发,因为 CPU 可以快速切换线程:

1
2
3
task1 执行一点
切换到 task2 执行一点
再切换回 task1

由于切换速度很快,人感觉像是同时执行。

4. 并发和并行的区别

对比项 并发 并行
关注点 任务调度能力 同时执行能力
是否必须多核 不必须 通常需要多核
是否同一时刻执行 不一定
类比 一个人轮流处理多个窗口 多个人同时处理多个窗口

一句话:

并发是“看起来一起做”,并行是“真的一起做”。

5. 同步

同步是调用方发起任务后,要等待结果返回,才能继续往下执行。

1
2
String result = queryUser();
System.out.println(result);

queryUser() 不返回,下面代码就不执行。

同步强调的是调用方等待结果。

6. 异步

异步是调用方发起任务后,不需要一直等待结果,可以先去做别的事情。

1
2
3
4
CompletableFuture.supplyAsync(() -> queryUser())
.thenAccept(result -> System.out.println(result));

System.out.println("先执行其他逻辑");

异步强调的是调用方不阻塞。

7. 同步、异步和阻塞、非阻塞的区别

很多人会把同步/异步和阻塞/非阻塞混在一起,其实它们关注点不同。

概念 关注点
同步/异步 关注结果通知方式
阻塞/非阻塞 关注当前线程是否被挂起等待

例如:

同步阻塞:

1
String result = rpcCall();

异步非阻塞:

1
CompletableFuture<String> future = rpcCallAsync();

同步非阻塞也存在,例如不断轮询结果:

1
2
3
while (!future.isDone()) {
// 当前线程没有被挂起,但是一直主动检查
}

这就像你点外卖:

模式 类比
同步阻塞 站在店门口等饭做好
同步非阻塞 每隔一分钟问一次饭好了没
异步阻塞 留电话,但你坐着啥也不干
异步非阻塞 留电话,然后去干别的,饭好了通知你

三、临界区、锁、原子操作

1. 临界区

临界区是访问共享资源的代码区域。

例如:

1
count++;

如果 count 是多个线程共享的变量,那么这行代码就是临界区的一部分。

因为 count++ 不是一步完成的,它大概包含:

1
2
3
1. 读取 count
2. count + 1
3. 写回 count

多个线程同时执行,就可能发生数据覆盖。

2. 锁

锁是保护临界区的一种机制。

1
2
3
synchronized (this) {
count++;
}

锁的作用是:

同一时刻只允许一个线程进入临界区。

常见锁:

说明
synchronized Java 内置锁,基于对象监视器
ReentrantLock JUC 可重入锁,支持公平锁、尝试加锁、可中断
ReadWriteLock 读写锁,读读共享,读写互斥
StampedLock 支持乐观读的锁
分布式锁 跨进程、跨服务控制并发

3. 原子操作

原子操作是不可再分割的操作。

要么全部执行成功,要么完全不执行,中间不会被其他线程打断看到半成品状态。

例如:

1
2
3
AtomicInteger count = new AtomicInteger(0);

count.incrementAndGet();

AtomicInteger 底层通常基于 CAS 实现。

4. 临界区、锁、原子操作的区别

概念 解决什么问题 举例
临界区 哪段代码有并发风险 count++
如何保护临界区 synchronizedReentrantLock
原子操作 单个操作不可分割 AtomicInteger.incrementAndGet()

锁是悲观思路:

我认为会有冲突,所以先加锁。

CAS 是乐观思路:

我先尝试修改,如果没人改过就成功,否则重试。


四、死锁、饥饿、活锁

1. 死锁

死锁是多个线程互相等待对方释放资源,导致所有线程都无法继续执行。

典型例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
Object lockA = new Object();
Object lockB = new Object();

Thread t1 = new Thread(() -> {
synchronized (lockA) {
sleep(100);
synchronized (lockB) {
System.out.println("t1 done");
}
}
});

Thread t2 = new Thread(() -> {
synchronized (lockB) {
sleep(100);
synchronized (lockA) {
System.out.println("t2 done");
}
}
});

t1 拿着 lockAlockBt2 拿着 lockBlockA,两边都不让,程序就僵住了。

这就像两个人过窄桥,都说“你先退”,但谁也不退。

2. 死锁四个必要条件

死锁产生通常需要同时满足四个条件:

条件 含义
互斥条件 资源一次只能被一个线程占用
请求与保持条件 线程已经持有资源,又继续请求其他资源
不可剥夺条件 线程持有的资源不能被强制抢走
循环等待条件 多个线程形成头尾相接的资源等待环

只要破坏其中一个条件,就可以预防死锁。

3. 饥饿

饥饿是某个线程长期得不到执行机会或资源。

例如:

1
ReentrantLock lock = new ReentrantLock(false);

非公平锁下,新来的线程可能插队成功,老线程可能长期拿不到锁。

饥饿不一定是程序完全卡死,而是某些线程“老是轮不到”。

4. 活锁

活锁是线程没有阻塞,也一直在运行,但因为互相让步,导致任务始终无法完成。

比如两个人在走廊相遇:

1
2
3
4
5
A 往左让
B 也往左让

A 往右让
B 也往右让

两个人都在动,但就是过不去。

死锁是“都不动了”,活锁是“都在动但没进展”。

5. 死锁、饥饿、活锁对比

问题 表现 是否还在运行 根本原因
死锁 互相等待 不运行了 资源等待环
饥饿 某线程长期拿不到资源 其他线程运行 调度不公平或优先级问题
活锁 一直重试但没进展 还在运行 过度让步或冲突处理策略错误

五、死锁预防、避免、检测、解除

1. 死锁预防

死锁预防是在设计时破坏死锁四个必要条件之一。

常见方式:

破坏请求与保持条件

一次性申请所有资源。

1
2
3
4
5
synchronized (lockA) {
synchronized (lockB) {
// do something
}
}

但要确保所有线程申请锁的顺序一致。

破坏不可剥夺条件

如果获取不到后续资源,就释放已经持有的资源。

1
2
3
4
5
6
7
8
9
10
11
12
13
if (lockA.tryLock()) {
try {
if (lockB.tryLock()) {
try {
// do something
} finally {
lockB.unlock();
}
}
} finally {
lockA.unlock();
}
}

破坏循环等待条件

规定锁的获取顺序。

比如所有线程都必须先拿 lockA,再拿 lockB

这是业务开发中最常用的方式。

2. 死锁避免

死锁避免是在运行过程中判断这次资源分配是否安全。

经典算法是银行家算法。

它的思想是:

分配资源之前先判断,分配之后系统是否仍然处于安全状态。

如果不安全,就暂时不分配。

在 Java 业务开发中,我们一般不会直接实现银行家算法,但这个思想可以用在资源池设计中。

例如:

场景 避免策略
数据库连接池 限制最大连接数
线程池 控制队列和线程数量
分布式锁 设置超时时间
RPC 调用 设置超时、熔断、限流

3. 死锁检测

死锁检测是系统运行过程中发现已经出现死锁。

Java 中可以用工具检测:

1
jstack <pid>

如果存在 Java 级别死锁,线程 dump 里可能会出现:

1
Found one Java-level deadlock

也可以用:

工具 说明
jstack 查看线程堆栈
jconsole 图形化监控
jvisualvm 查看线程状态
Arthas 在线诊断 Java 应用
JFR Java Flight Recorder

4. 死锁解除

死锁解除是在死锁发生后恢复系统。

常见方式:

方式 说明
中断线程 对可中断锁有效,例如 lockInterruptibly()
超时退出 获取锁设置超时时间
回滚事务 数据库场景常见
重启服务 最粗暴但有时有效
人工释放资源 运维处理资源占用

示例:

1
2
3
4
5
6
7
8
9
if (lock.tryLock(3, TimeUnit.SECONDS)) {
try {
// do something
} finally {
lock.unlock();
}
} else {
System.out.println("获取锁超时,放弃执行");
}

5. 四者区别

名称 发生阶段 核心思想
死锁预防 设计阶段 破坏死锁条件
死锁避免 运行前/运行中 判断是否安全再分配
死锁检测 运行中 发现是否已经死锁
死锁解除 死锁后 恢复系统执行

六、线程安全、原子性、可见性、有序性

1. 线程安全

线程安全是指多个线程同时访问同一段代码或同一个对象时,程序结果仍然正确。

例如:

1
2
3
4
5
6
7
8
9
10
11
class Counter {
private int count = 0;

public void increment() {
count++;
}

public int getCount() {
return count;
}
}

这个类不是线程安全的。

多个线程同时执行 increment(),最终结果可能小于预期。

2. 原子性

原子性是指一个操作不可被打断。

例如:

1
count++;

不是原子操作。

因为它至少包含三步:

1
2
3
读取 count
加 1
写回 count

解决方式:

1
2
3
synchronized (this) {
count++;
}

或者:

1
2
3
AtomicInteger count = new AtomicInteger();

count.incrementAndGet();

3. 可见性

可见性是指一个线程修改了共享变量,其他线程能够及时看到。

问题示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Demo {
private boolean running = true;

public void stop() {
running = false;
}

public void run() {
while (running) {
// do something
}
}
}

一个线程调用 stop() 后,另一个线程未必马上看到 running = false

可以使用 volatile

1
private volatile boolean running = true;

volatile 可以保证可见性。

4. 有序性

有序性是指程序执行顺序在多线程环境下看起来符合预期。

为了优化性能,编译器和 CPU 可能会进行指令重排序。

单线程下,重排序不会改变最终结果。

多线程下,重排序可能导致意想不到的问题。

经典问题是双重检查锁:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Singleton {
private static volatile Singleton instance;

private Singleton() {}

public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}

这里 volatile 不仅保证可见性,也禁止特定的指令重排序。

5. 三大问题总结

问题 含义 典型解决方式
原子性 操作不能被打断 synchronizedLock、Atomic 类
可见性 修改能被其他线程看到 volatilesynchronizedLock
有序性 执行顺序符合预期 volatilesynchronized、JMM happens-before

七、JMM:Java 内存模型

1. JMM 是什么

JMM,全称 Java Memory Model,Java 内存模型。

它不是 JVM 内存结构,也不是堆、栈、方法区那套东西。

JMM 主要定义:

多线程环境下,线程如何与主内存交互,以及一个线程对共享变量的修改,什么时候能被另一个线程看到。

JMM 解决的是并发中的三类问题:

问题 说明
原子性 操作是否不可分割
可见性 修改是否能被其他线程看到
有序性 指令是否会被重排序影响

2. 主内存和工作内存

JMM 可以抽象理解为:

区域 说明
主内存 共享变量存放的位置,可以近似理解为堆中的共享数据
工作内存 每个线程自己的变量副本,可以近似理解为 CPU 缓存、寄存器等

线程不能直接操作主内存中的变量,而是先把变量拷贝到自己的工作内存中,修改后再同步回主内存。

示意:

1
2
3
4
主内存:count = 0

线程 A 工作内存:count = 0
线程 B 工作内存:count = 0

如果两个线程都对 count++,可能发生:

1
2
3
4
5
线程 A 读取 count = 0
线程 B 读取 count = 0

线程 A 写回 count = 1
线程 B 写回 count = 1

最终结果是 1,而不是 2。

3. happens-before

JMM 中非常重要的概念是 happens-before

它表示:

如果操作 A happens-before 操作 B,那么 A 的执行结果对 B 可见,并且 A 的执行顺序排在 B 之前。

常见 happens-before 规则:

规则 说明
程序顺序规则 同一线程内,前面的操作 happens-before 后面的操作
监视器锁规则 unlock happens-before 后续对同一把锁的 lock
volatile 规则 对 volatile 变量的写 happens-before 后续对该变量的读
线程启动规则 Thread.start() happens-before 线程内的动作
线程终止规则 线程内动作 happens-before 其他线程检测到它终止
线程中断规则 interrupt() happens-before 被中断线程检测到中断
传递性 A happens-before B,B happens-before C,则 A happens-before C

4. volatile 的作用

volatile 主要有两个作用:

作用 说明
保证可见性 一个线程修改后,其他线程能看到
禁止特定重排序 防止读写 volatile 前后的指令被乱序优化

volatile 不保证复合操作的原子性。

错误示例:

1
2
3
4
5
private volatile int count = 0;

public void increment() {
count++;
}

虽然 count 是 volatile,但 count++ 仍然不是原子操作。

5. synchronized 的作用

synchronized 同时可以保证:

能力 说明
原子性 同一时刻只有一个线程进入临界区
可见性 解锁前修改会刷新,后续加锁线程可见
有序性 基于 happens-before 规则

所以 synchronized 是一个比较完整的并发控制工具。

6. JMM 和 JVM 内存结构的区别

对比项 JMM JVM 内存结构
关注点 多线程可见性、有序性、原子性 JVM 运行时内存区域
内容 主内存、工作内存、happens-before 堆、栈、方法区、程序计数器
解决问题 并发安全 程序运行时数据存放
是否真实物理划分 抽象规范 JVM 运行时区域

一句话:

JVM 内存结构讲对象和方法放在哪里;JMM 讲多线程读写共享变量时怎么保证正确性。


八、Java 程序执行流程示例

假设代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public class Main {
public static void main(String[] args) {
Employee emp = new Employee();
emp.setName("老齐");
emp.setAge(13);

Department dept = new Department();
dept.setDname("小卖部");

emp.setDepartment(dept);

emp.sayJoke("一言不合...");
}
}

1. 加载类:ClassLoader

程序启动后,JVM 会通过类加载器加载相关类:

1
2
3
Main.class
Employee.class
Department.class

类加载大致分为:

阶段 说明
加载 读取 class 文件,生成 Class 对象
验证 验证字节码是否合法
准备 为静态变量分配内存并设置默认值
解析 将符号引用转为直接引用
初始化 执行静态变量赋值和 static 代码块

例如:

1
static int count = 10;

准备阶段 count 是默认值 0,初始化阶段才变成 10

2. 启动 main 方法

JVM 创建 main 线程,调用:

1
public static void main(String[] args)

此时主线程有自己的虚拟机栈。

main 方法会创建一个栈帧,里面存放局部变量表、操作数栈等信息。

3. 执行 emp = new Employee()

这一步大致包括:

1
2
3
4
5
1. 检查 Employee 类是否已加载
2. 在堆中为 Employee 对象分配内存
3. 对对象字段设置默认值
4. 执行构造方法
5. 将对象引用赋值给 emp

如果 Employee 类是:

1
2
3
4
5
public class Employee {
private String name;
private int age;
private Department department;
}

对象刚分配时默认值为:

1
2
3
name = null
age = 0
department = null

4. 执行 emp.setName("老齐")

1
emp.setName("老齐");

含义是:

1
2
3
1. main 栈帧中的 emp 引用指向堆中的 Employee 对象
2. 调用 Employee 对象的 setName 方法
3. 将 name 字段设置为 "老齐"

字符串 "老齐" 是一个字符串对象,通常在字符串常量池中有对应引用。

5. 执行 emp.setAge(13)

1
emp.setAge(13);

把堆中 Employee 对象的 age 字段改成 13

此时 Employee 对象状态:

1
2
3
name = "老齐"
age = 13
department = null

6. 执行 dept = new Department()

JVM 在堆中创建 Department 对象:

1
Department dept = new Department();

如果 Department 是:

1
2
3
public class Department {
private String dname;
}

初始状态:

1
dname = null

7. 执行 dept.setDname("小卖部")

把 Department 对象的 dname 字段设置为 "小卖部"

1
2
dept -> Department 对象
Department.dname = "小卖部"

8. 执行 emp.setDepartment(dept)

1
emp.setDepartment(dept);

这一步不是把 Department 对象复制一份给 Employee,而是把 dept 引用赋值给 emp.department

最终对象关系:

1
2
3
4
5
6
7
8
9
10
11
main 栈帧:
emp -----> Employee 对象
dept -----> Department 对象

Employee 对象:
name = "老齐"
age = 13
department -----> Department 对象

Department 对象:
dname = "小卖部"

9. 执行 emp.sayJoke("一言不合...")

调用对象方法:

1
emp.sayJoke("一言不合...");

JVM 会创建 sayJoke 方法对应的新栈帧。

方法执行时,可以访问:

数据 来源
this 当前 Employee 对象
参数 joke "一言不合..."
成员变量 name、age、department 堆中 Employee 对象字段

10. 方法执行完成

sayJoke 方法执行完后,它的栈帧出栈。

main 方法继续执行,最终 main 方法结束,main 栈帧出栈。

如果没有其他非守护线程,JVM 进程退出。

11. 这段代码和并发有什么关系?

单线程执行时,这段代码基本没有线程安全问题。

但如果多个线程共享同一个 Employee 对象:

1
2
3
4
Employee emp = new Employee();

new Thread(() -> emp.setAge(18)).start();
new Thread(() -> emp.setName("老王")).start();

这时就涉及:

问题 说明
可见性 一个线程修改字段,另一个线程是否能看到
原子性 多个字段更新是否作为整体完成
有序性 对象发布时是否可能被看到半初始化状态
线程安全 对象是否允许被多个线程安全访问

所以对象创建本身不复杂,复杂的是对象被多个线程共享之后。


九、线程的五种状态、六种状态、七种状态

1. 为什么有多种说法?

线程状态有不同的划分方式。

常见说法:

说法 来源
五种状态 操作系统或教学模型
六种状态 Java 官方 Thread.State
七种状态 把 Runnable 拆成 Ready 和 Running 的细化模型

写 Java 博客时,建议重点以 Java 官方 6 种状态为准。

2. Java 官方六种状态

Java Thread.State 有 6 种:

状态 说明
NEW 新建,还没有调用 start()
RUNNABLE 可运行,可能正在运行,也可能等待 CPU 调度
BLOCKED 阻塞,等待进入 synchronized 临界区
WAITING 无限期等待,需要其他线程唤醒
TIMED_WAITING 限时等待,时间到了会自动返回
TERMINATED 线程执行结束

3. NEW

线程对象被创建,但还没有启动。

1
2
3
Thread thread = new Thread(() -> {
System.out.println("hello");
});

此时状态是 NEW

4. RUNNABLE

调用 start() 后,线程进入 RUNNABLE。

1
thread.start();

注意:

Java 的 RUNNABLE 包含两个含义:

1
2
就绪:等待 CPU 调度
运行:正在 CPU 上执行

所以 Java 里没有单独的 READY 和 RUNNING 状态。

5. BLOCKED

线程等待进入 synchronized 代码块时,会进入 BLOCKED。

1
2
3
synchronized (lock) {
// 一个线程持有锁
}

另一个线程也想进入同一个锁保护的代码块,就会 BLOCKED。

注意:

BLOCKED 主要针对 synchronized 的 monitor lock。

6. WAITING

线程无限期等待其他线程唤醒。

常见方式:

1
2
3
object.wait();
thread.join();
LockSupport.park();

例如:

1
2
3
synchronized (lock) {
lock.wait();
}

此时线程等待其他线程调用:

1
lock.notify();

或者:

1
lock.notifyAll();

7. TIMED_WAITING

线程限时等待。

常见方式:

1
2
3
4
Thread.sleep(1000);
object.wait(1000);
thread.join(1000);
LockSupport.parkNanos(...);

时间到了之后,线程会自动恢复到可竞争 CPU 的状态。

8. TERMINATED

线程执行完成,进入终止状态。

1
2
3
4
5
Thread thread = new Thread(() -> {
System.out.println("done");
});

thread.start();

任务执行完后,线程状态就是 TERMINATED。

线程终止后不能再次调用 start()

否则会抛出:

1
IllegalThreadStateException

9. 五种状态模型

五种状态通常是:

1
新建 -> 就绪 -> 运行 -> 阻塞 -> 终止
状态 说明
新建 线程创建
就绪 等待 CPU
运行 正在执行
阻塞 等待锁、I/O、sleep、wait 等
终止 执行结束

这是操作系统或教学模型,比较宏观。

10. 七种状态模型

七种状态通常是:

1
2
3
4
5
6
7
新建
就绪
运行
阻塞
等待
超时等待
终止

它比五种状态更细,把阻塞类状态拆成:

状态 说明
阻塞 等锁
等待 无限等待
超时等待 限时等待

11. 五种、六种、七种状态映射

教学七状态 Java 官方状态
新建 NEW
就绪 RUNNABLE
运行 RUNNABLE
阻塞 BLOCKED
等待 WAITING
超时等待 TIMED_WAITING
终止 TERMINATED

结论:

Java 官方是 6 种状态;所谓 7 种状态,是把 RUNNABLE 拆成“就绪”和“运行”之后的教学模型。


十、创建线程的几种方式

Java 中常见创建线程方式有四类:

1
2
3
4
1. 继承 Thread 类
2. 实现 Runnable 接口
3. 实现 Callable 接口 + Future/FutureTask
4. 通过线程池创建线程

实际开发中,最推荐的是线程池。


1. 继承 Thread 类

示例

1
2
3
4
5
6
7
8
9
10
11
12
public class MyThread extends Thread {

@Override
public void run() {
System.out.println("当前线程:" + Thread.currentThread().getName());
}

public static void main(String[] args) {
MyThread thread = new MyThread();
thread.start();
}
}

注意:

调用 start() 才是启动新线程。

如果直接调用 run()

1
thread.run();

那只是普通方法调用,不会创建新线程。

优点

优点 说明
简单直观 适合初学者理解线程
可以直接使用 Thread 方法 比如 getName()interrupt()

缺点

缺点 说明
Java 单继承限制 继承 Thread 后不能再继承其他类
任务和线程绑定太死 不利于复用
不适合实际业务开发 难以统一管理线程资源

使用场景

适合学习线程基本概念,不推荐在生产业务中大量使用。


2. 实现 Runnable 接口

示例

1
2
3
4
5
6
7
8
9
10
11
12
public class MyRunnable implements Runnable {

@Override
public void run() {
System.out.println("当前线程:" + Thread.currentThread().getName());
}

public static void main(String[] args) {
Thread thread = new Thread(new MyRunnable());
thread.start();
}
}

Lambda 写法:

1
2
3
4
5
Thread thread = new Thread(() -> {
System.out.println("hello runnable");
});

thread.start();

优点

优点 说明
避免单继承限制 类还可以继承其他父类
任务和线程分离 Runnable 表示任务,Thread 表示执行者
更适合资源共享 多个线程可以执行同一个 Runnable

缺点

缺点 说明
没有返回值 run() 返回 void
不能直接抛出受检异常 需要自己捕获处理

使用场景

适合不需要返回结果的异步任务。


3. Callable + Future

Runnable 没有返回值,也不能方便地抛出异常。

如果任务需要返回结果,可以使用 Callable

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

public class CallableDemo {

public static void main(String[] args) throws Exception {
Callable<Integer> callable = () -> {
Thread.sleep(1000);
return 100;
};

FutureTask<Integer> futureTask = new FutureTask<>(callable);

Thread thread = new Thread(futureTask);
thread.start();

Integer result = futureTask.get();

System.out.println("结果:" + result);
}
}

优点

优点 说明
有返回值 call() 可以返回结果
可以抛出异常 便于任务失败处理
可以配合 Future 获取结果 支持取消、判断完成状态

缺点

缺点 说明
get() 可能阻塞 如果任务没完成,调用线程会等待
单独使用不够优雅 实际开发通常配合线程池

使用场景

适合需要异步计算结果的任务。


4. 通过线程池创建线程

线程池是实际开发中最常用的方式。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadPoolDemo {

public static void main(String[] args) {
ExecutorService executor = Executors.newFixedThreadPool(4);

executor.submit(() -> {
System.out.println("当前线程:" + Thread.currentThread().getName());
});

executor.shutdown();
}
}

不过,生产环境不建议直接使用 Executors.newFixedThreadPool() 这类快捷工厂方法,因为很多默认参数不够明确,可能导致队列过大或线程过多。

更推荐手动创建 ThreadPoolExecutor

ThreadPoolExecutor 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import java.util.concurrent.*;

public class ThreadPoolExecutorDemo {

public static void main(String[] args) {
ThreadPoolExecutor executor = new ThreadPoolExecutor(
4,
8,
60,
TimeUnit.SECONDS,
new ArrayBlockingQueue<>(100),
new ThreadFactory() {
private int index = 1;

@Override
public Thread newThread(Runnable r) {
return new Thread(r, "biz-pool-" + index++);
}
},
new ThreadPoolExecutor.CallerRunsPolicy()
);

executor.submit(() -> {
System.out.println("执行业务任务");
});

executor.shutdown();
}
}

核心参数

参数 说明
corePoolSize 核心线程数
maximumPoolSize 最大线程数
keepAliveTime 非核心线程空闲存活时间
unit 时间单位
workQueue 任务队列
threadFactory 线程工厂
handler 拒绝策略

执行流程

线程池提交任务后,大致流程:

1
2
3
4
5
6
7
8
9
10
11
1. 当前线程数 < corePoolSize
创建核心线程执行任务

2. 当前线程数 >= corePoolSize
任务进入队列

3. 队列满,并且当前线程数 < maximumPoolSize
创建非核心线程执行任务

4. 队列满,并且当前线程数 >= maximumPoolSize
执行拒绝策略

常见拒绝策略

策略 说明
AbortPolicy 直接抛异常,默认策略
CallerRunsPolicy 由提交任务的线程自己执行
DiscardPolicy 直接丢弃任务,不抛异常
DiscardOldestPolicy 丢弃队列中最老的任务

优点

优点 说明
复用线程 避免频繁创建销毁线程
控制并发量 防止线程无限增长
统一管理 方便监控、关闭、命名
支持任务队列 高峰期可缓冲任务

缺点

缺点 说明
参数配置复杂 配不好容易 OOM 或任务堆积
需要监控 需要关注队列长度、活跃线程数
任务隔离要求高 不同业务最好使用不同线程池

使用场景

实际业务开发中,线程池适合:

场景 示例
异步处理 发短信、发 MQ、写日志
批量任务 批量计算、批量导入
I/O 并发 调用多个远程接口
定时任务 周期性数据同步
限流隔离 防止某类任务拖垮主线程

十一、虚拟线程创建方式

虽然传统线程创建方式有四种,但 Java 21 之后还需要了解虚拟线程。

1. 直接创建虚拟线程

1
2
3
Thread.startVirtualThread(() -> {
System.out.println("虚拟线程:" + Thread.currentThread());
});

2. 使用 Builder 创建

1
2
3
4
5
Thread thread = Thread.ofVirtual()
.name("virtual-worker-", 1)
.start(() -> {
System.out.println("hello virtual thread");
});

3. 使用虚拟线程执行器

1
2
3
4
5
6
7
8
9
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
for (int i = 0; i < 1000; i++) {
executor.submit(() -> {
// 模拟 I/O 任务
Thread.sleep(1000);
return "ok";
});
}
}

4. 虚拟线程使用建议

适合:

1
2
3
4
大量阻塞式 I/O
大量 HTTP 请求
大量数据库查询
大量 RPC 调用

不适合:

1
2
3
4
CPU 密集型计算
依赖大量 ThreadLocal 的场景
无限制访问数据库连接池
把虚拟线程当成无限资源滥用

虚拟线程不是银弹。

如果数据库连接池只有 20 个连接,你开 10000 个虚拟线程同时查库,最终瓶颈还是数据库连接池。


十二、JUC 入门

JUC 是 java.util.concurrent 的简称,是 Java 并发编程的核心工具包。

它大致可以分为几类:

1
2
3
4
5
6
7
1. 线程池与任务执行
2. 锁机制
3. 原子类
4. 并发容器
5. 阻塞队列
6. 并发协作工具
7. 异步编排工具

1. 线程池相关

核心类:

说明
Executor 最基础任务执行接口
ExecutorService 支持提交任务、关闭线程池
ThreadPoolExecutor 普通线程池核心实现
ScheduledThreadPoolExecutor 定时任务线程池
ForkJoinPool 分治任务线程池
CompletableFuture 异步任务编排

示例:

1
2
3
4
5
6
7
8
9
ExecutorService executor = Executors.newFixedThreadPool(4);

Future<Integer> future = executor.submit(() -> {
return 1 + 1;
});

Integer result = future.get();

executor.shutdown();

2. 锁相关

核心类:

说明
Lock 锁接口
ReentrantLock 可重入锁
ReadWriteLock 读写锁接口
ReentrantReadWriteLock 可重入读写锁
StampedLock 支持乐观读
Condition 条件队列
LockSupport 线程挂起和唤醒工具

ReentrantLock 示例

1
2
3
4
5
6
7
8
9
10
private final ReentrantLock lock = new ReentrantLock();

public void update() {
lock.lock();
try {
// 临界区
} finally {
lock.unlock();
}
}

必须在 finally 中释放锁。

否则异常发生后锁不释放,其他线程可能永远等不到。


3. 原子类

核心类:

说明
AtomicInteger 原子 int
AtomicLong 原子 long
AtomicBoolean 原子 boolean
AtomicReference 原子对象引用
LongAdder 高并发计数优化
LongAccumulator 自定义累加规则

示例:

1
2
3
AtomicInteger count = new AtomicInteger();

count.incrementAndGet();

高并发计数推荐:

1
2
3
4
5
LongAdder adder = new LongAdder();

adder.increment();

long result = adder.sum();

AtomicLong 适合一般并发计数,LongAdder 适合高并发热点计数。


4. 并发容器

核心类:

说明
ConcurrentHashMap 并发 Map
CopyOnWriteArrayList 写时复制 List
ConcurrentLinkedQueue 非阻塞并发队列
BlockingQueue 阻塞队列接口

ConcurrentHashMap 示例

1
2
3
4
5
ConcurrentHashMap<String, Integer> map = new ConcurrentHashMap<>();

map.put("A", 1);

map.compute("A", (key, oldValue) -> oldValue == null ? 1 : oldValue + 1);

不要在并发场景下直接使用普通 HashMap 做共享写入。


5. 阻塞队列

核心类:

说明
ArrayBlockingQueue 有界数组阻塞队列
LinkedBlockingQueue 链表阻塞队列
PriorityBlockingQueue 优先级阻塞队列
DelayQueue 延迟队列
SynchronousQueue 不存储元素的移交队列

阻塞队列常用于生产者消费者模型。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
BlockingQueue<String> queue = new ArrayBlockingQueue<>(100);

new Thread(() -> {
try {
queue.put("task");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();

new Thread(() -> {
try {
String task = queue.take();
System.out.println(task);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}).start();

6. 并发协作工具

核心类:

说明
CountDownLatch 一个线程等待多个线程完成
CyclicBarrier 多个线程互相等待,到齐后继续
Semaphore 控制同时访问资源的线程数量
Phaser 更灵活的阶段协作工具
Exchanger 两个线程交换数据

CountDownLatch 示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
CountDownLatch latch = new CountDownLatch(3);

for (int i = 0; i < 3; i++) {
new Thread(() -> {
try {
System.out.println("子任务完成");
} finally {
latch.countDown();
}
}).start();
}

latch.await();

System.out.println("所有任务完成");

适合主线程等待多个子任务完成。

Semaphore 示例

1
2
3
4
5
6
7
8
9
10
Semaphore semaphore = new Semaphore(5);

public void access() throws InterruptedException {
semaphore.acquire();
try {
// 同时最多 5 个线程执行
} finally {
semaphore.release();
}
}

适合限流,比如限制同时导出文件、同时调用外部接口的数量。


7. CompletableFuture

CompletableFuture 适合异步编排。

示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
CompletableFuture<String> userFuture = CompletableFuture.supplyAsync(() -> {
return "用户信息";
});

CompletableFuture<String> orderFuture = CompletableFuture.supplyAsync(() -> {
return "订单信息";
});

CompletableFuture<Void> all = CompletableFuture.allOf(userFuture, orderFuture);

all.join();

System.out.println(userFuture.join());
System.out.println(orderFuture.join());

常见方法:

方法 说明
supplyAsync 异步执行,有返回值
runAsync 异步执行,无返回值
thenApply 转换结果
thenAccept 消费结果
thenCompose 串联异步任务
thenCombine 合并两个异步任务
allOf 等待多个任务全部完成
anyOf 任意一个任务完成即可
exceptionally 异常处理

十三、线程池实战建议

1. 不同业务使用不同线程池

不要所有异步任务都丢到一个线程池。

例如:

1
2
3
4
5
订单线程池
库存线程池
短信线程池
报表线程池
文件导入线程池

这样可以避免一个慢任务拖垮所有业务。

2. 线程必须命名

线程名很重要。

错误示例:

1
pool-1-thread-1

线上排查问题时很痛苦。

推荐:

1
2
3
order-export-pool-1
finance-calc-pool-1
mq-consume-pool-1

3. 队列不要无限大

不要轻易使用无界队列。

无界队列可能导致任务无限堆积,最终 OOM。

推荐使用有界队列:

1
new ArrayBlockingQueue<>(1000)

4. 设置合理拒绝策略

业务重要任务不要静默丢弃。

一般建议:

业务类型 策略
核心业务 AbortPolicy,快速失败
可降级任务 CallerRunsPolicy
日志、埋点 可考虑丢弃,但要有监控
批处理任务 失败重试或落库补偿

5. 监控线程池

至少监控:

1
2
3
4
5
6
7
核心线程数
最大线程数
活跃线程数
队列长度
任务完成数
拒绝任务数
任务执行耗时

线程池没有监控,就像开车没有仪表盘。 不是不能开,是迟早撞树。

参考资料

启示录

富贵岂由人,时会高志须酬。

能成功于千载者,必以近察远。


Java高并发编程
https://allendericdalexander.github.io/2026/05/31/java_multi_thread_coding/
作者
AtLuoFu
发布于
2026年5月31日
许可协议