Java 并发编程筑基篇
Java 并发编程筑基篇知识文档
适用读者:Java 基础复习 / 校招面试 / 工程实践入门
维护说明:本文档按"基础概念 → 底层原理 → 工程工具 → 实战练习"组织,可直接作为复习大纲。
目录
第一部分:基础概念
1.1 进程、线程、协程
这三个是"代码执行单元"的三个粒度,从大到小。
进程(Process)
- 定义:操作系统资源分配的基本单位。一个运行的程序就是一个进程。
- 特征:拥有独立的内存空间(虚拟地址空间)、文件描述符、堆栈等。进程间内存隔离,通信(IPC)成本高。
- 例子:开一个 IDEA 是一个进程,开一个 Chrome 又是一个进程。
线程(Thread)
- 定义:CPU 调度的基本单位,是进程内的执行流。一个进程至少有一个线程(主线程)。
- 特征:同一进程内的线程共享堆、方法区,但各自有独立的栈、程序计数器(PC)、寄存器。
- 关键区别:线程是"谁占用 CPU"的最小单位,进程是"拥有资源"的单位。
协程(Coroutine / Fiber)
- 定义:用户态的轻量级执行单元,由程序(而非操作系统)调度。也叫"微线程"“绿色线程”。
- 特征:
- 切换不进入内核态,成本极低(几十纳秒级 vs 线程微秒级)。
- 在单线程内协作式调度(主动让出 yield),不需要锁。
- 一个线程可跑成千上万个协程。
- 谁在用:Go 的 goroutine、Python 的 async/await、Kotlin 协程、Java 21 的虚拟线程(Virtual Thread / Project Loom)。
三者对比
| 维度 | 进程 | 线程 | 协程 |
|---|---|---|---|
| 调度者 | 操作系统 | 操作系统 | 用户程序 |
| 切换成本 | 高(内核态) | 中(内核态) | 低(用户态) |
| 内存隔离 | 独立 | 共享堆 | 共享(同线程内) |
| 通信方式 | IPC(管道/信号/共享内存) | 共享内存+锁 | 直接访问(无锁) |
| 数量上限 | 几十~几百 | 几百~几千 | 几万~几十万 |
💡 工程视角:Java 长期只有"线程",协程要靠第三方(Quasar)或反应式框架(Reactor/WebFlux)模拟;Java 21 的虚拟线程补上了这块短板,让写同步阻塞代码也能享受协程的并发能力。
1.2 并发、并行、串行、同步、异步
这是两组概念,容易混在一起,要拆开理解。
执行方式维度:并发 vs 并行 vs 串行
- 串行(Serial):任务一个接一个执行,A 完成才做 B。单核单线程天然串行。
- 并发(Concurrency):多个任务在"同一时间段"内推进,宏观上同时,微观上可能交替(时间片轮转)。强调"处理多件事的能力"。
- 并行(Parallelism):多个任务在"同一时刻"真正同时执行,必须有多核/多 CPU。强调"同时做多件事"。
🎯 经典比喻(Rob Pike):
- 并发 = 一个咖啡机,两个人轮流用(交替处理)。
- 并行 = 两个咖啡机,两个人同时用。
口诀:并发是"同时应对",并行是"同时执行"。并发是结构设计,并行是硬件执行。
- 关系:并发是目标(应对多任务),并行是手段(真正同时跑)。单核也能并发(靠时间片),但单核无法并行(多线程任务)。
调用方式维度:同步 vs 异步
关注的是调用者要不要等结果。
- 同步(Synchronous):调用后主动等待结果返回,期间阻塞当前执行流。
- 异步(Asynchronous):调用后立即返回,结果通过回调、Future、事件等在将来通知。
注意:同步/异步 与 阻塞/非阻塞 是两组概念,常被混用。
- 同步/异步 → 关注调用者是否主动等结果。
- 阻塞/非阻塞 → 关注被调用者在等待期间是否挂起调用线程。
- 组合如:同步阻塞(BIO)、同步非阻塞(NIO)、异步非阻塞(AIO)。
一句话串联
用多线程是为了并发(应对多任务),在多核上自然就并行了;当某个任务要等 I/O,我们希望它异步,别把线程卡死。
1.3 线程原理
1. 线程的生命周期(OS 层)
- 新建 → 就绪 → 运行 → 阻塞 → 终止
- 就绪态的线程等待 CPU 时间片;拿到时间片就运行;时间片用完或被抢占就回就绪;等 I/O、锁、sleep 时进入阻塞。
2. Java 线程的六种状态(Thread.State)
| 状态 | 含义 |
|---|---|
NEW |
创建了 Thread 对象,还没 start() |
RUNNABLE |
在 JVM 看来"可运行"(包含 OS 的就绪+运行,Java 不区分) |
BLOCKED |
等待 synchronized 锁(监视器锁) |
WAITING |
无限等待(wait() / join() / LockSupport.park() 无超时) |
TIMED_WAITING |
有时限的等待(sleep(t) / wait(t) / join(t)) |
TERMINATED |
线程执行完毕 |
⚠️ 易错点:
BLOCKED只针对 synchronized 锁;用ReentrantLock等待时线程处于WAITING,不是BLOCKED。
3. 上下文切换(Context Switch)
线程从 A 切到 B 时,OS 要保存 A 的现场(PC、寄存器、栈指针)并恢复 B 的现场,这个动作叫上下文切换。
- 切换有成本(微秒级),且会破坏 CPU 缓存亲和性。
- 线程不是越多越好:线程过多 → 切换开销增大 → 反而变慢。这是线程池要控制大小的根本原因。
4. 线程的实现模型
- 内核级线程(1:1):一个用户线程对应一个内核线程。Java HotSpot 默认就是这种(Linux 上是 pthread)。调度由 OS 负责,功能强但切换贵。
- 用户级线程(N:1):多个用户线程映射到一个内核线程,库调度。轻但无法利用多核、阻塞会卡死全部。
- 混合模型(M:N):多个用户线程映射到多个内核线程。Go、Java 21 虚拟线程走这条路。
5. 线程安全与可见性原理
- 可见性:一个线程改了共享变量,另一个线程看不到 → 因为 CPU 缓存 + 指令重排序。解决:
volatile、synchronized、final。 - 原子性:操作不可分割。
i++不是原子的。解决:synchronized、AtomicXxx。 - 有序性:编译器/CPU 可能重排指令。
volatile插入内存屏障禁止重排。
1.4 Java 线程的创建方式
方式 1:继承 Thread 类
class MyThread extends Thread {
public void run() { System.out.println("running"); }
}
new MyThread().start();
- 缺点:Java 单继承,继承了
Thread就不能再继承别的类;耦合了"任务"和"执行机制"。
方式 2:实现 Runnable 接口(推荐)
Runnable task = () -> System.out.println("running");
new Thread(task).start();
- 优点:任务与执行分离,无单继承限制,适合共享资源。
Thread本身也实现了Runnable。
方式 3:实现 Callable + FutureTask(有返回值)
Callable<Integer> task = () -> 42;
FutureTask<Integer> future = new FutureTask<>(task);
new Thread(future).start();
Integer result = future.get(); // 阻塞获取结果
- 与
Runnable的区别:Callable的call()有返回值且能抛受检异常。 Future.get()会阻塞直到任务完成。
方式 4:线程池 ExecutorService(工程推荐)
ExecutorService pool = Executors.newFixedThreadPool(4);
Future<Integer> f = pool.submit(() -> 42);
pool.shutdown();
- 优点:复用线程、控制并发数、统一管理。
- ⚠️ 阿里规约:不要用
Executors的快捷方法(队列无界可能 OOM),要用ThreadPoolExecutor手动构造。
方式 5(Java 21+):虚拟线程
Thread.startVirtualThread(() -> doIo());
- 轻量、海量、适合 I/O 密集场景。
🎯 取舍口诀:无返回值用
Runnable,有返回值用Callable,生产环境一律走线程池,别裸new Thread()。
1.5 Java 线程重点方法
run() vs start()(最高频考点)
start() |
run() |
|
|---|---|---|
| 作用 | 启动新线程,由 JVM 调度其 run() |
普通方法调用 |
| 是否新建线程 | ✅ 是 | ❌ 否,在当前线程同步执行 |
| 能否多次调用 | ❌ 一个线程只能 start() 一次(再调抛 IllegalThreadStateException) |
✅ 可多次调用(普通方法) |
🚨 易错:
new Thread(task).run();不会启动新线程,只是在主线程里把run跑一遍 —— 这是常见 bug。
sleep(long millis)
- 静态方法(
Thread.sleep),让当前线程休眠,进入TIMED_WAITING。 - 不释放锁(这是和
wait()的核心区别)。 - 休眠期间被
interrupt()会抛InterruptedException。
wait() / notify() / notifyAll()(属于 Object,不是 Thread)
- 必须在 synchronized 块内调用(持有对象监视器),否则抛
IllegalMonitorStateException。 wait()会释放锁并进入WAITING(对象等待集);被唤醒后要重新竞争锁。- 用于生产者-消费者模型。
sleep vs wait 对比(必背):
sleep |
wait |
|
|---|---|---|
| 归属 | Thread 静态方法 |
Object 实例方法 |
| 释放锁 | 不释放 | 释放 |
| 唤醒 | 超时或 interrupt | notify/超时/interrupt |
| 前置条件 | 无 | 必须持有该对象锁 |
join() —— 等待线程结束
t.join(); // 当前线程阻塞,直到 t 执行完
- 内部用
wait()实现,进入WAITING/TIMED_WAITING。 - 典型场景:主线程等所有子线程算完再汇总。
yield() —— 礼让
- 提示调度器:当前线程愿意让出 CPU。只是建议,不保证会被切换。
- 进入就绪态(不是阻塞态),可能立刻又被调度。
interrupt() —— 中断协作(重要)
- Java 用协作式中断:不强制停止线程,而是设置中断标志,由线程自己决定如何响应。
- 检查:
Thread.currentThread().isInterrupted()(不清标志)、Thread.interrupted()(静态,会清标志)。 - 若线程正阻塞在
sleep/wait/join,会抛InterruptedException并清除标志。 - 正确写法:
while (!Thread.currentThread().isInterrupted()) {
// ... 干活
}
// 捕获 InterruptedException 后应:重新设置中断标志 或 退出
setDaemon(boolean) —— 守护线程
- 必须在
start()前设置。 - 守护线程(如 GC 线程)的特性:所有非守护线程结束,JVM 就退出,守护线程会被强制终止(不保证 finally 执行完)。
- 别在守护线程里做关键 I/O。
其他常用方法
| 方法 | 说明 |
|---|---|
getName()/setName() |
线程名 |
getPriority()/setPriority() |
优先级(1~10,默认 5,仅建议性) |
getId() |
线程唯一 ID |
Thread.currentThread() |
获取当前线程引用 |
getState() |
获取线程状态(六种之一) |
Thread.holdsLock(obj) |
判断当前线程是否持有某对象锁 |
第二部分:进阶原理
2.1 synchronized 与 Lock(AQS)
1. synchronized 的底层原理
**Monitor(监视器)**机制:每个 Java 对象天生带一个 Monitor(对应 C++ 的 ObjectMonitor)。
- 字节码层面:
- 同步代码块 →
monitorenter/monitorexit指令(正常退出 + 异常退出各一个monitorexit)。 - 同步方法 → 方法表中的
ACC_SYNCHRONIZED标志位。
- 同步代码块 →
ObjectMonitor关键字段:_owner(持有线程)、_EntryList(等锁队列 →BLOCKED)、_WaitSet(wait 队列 →WAITING)。- 加锁本质:线程进
_EntryList排队 → 抢到锁后_owner指向自己。
2. 锁升级(JDK 6 的重大优化,高频考点)
早期 synchronized 一律走 OS 互斥量(重量级),JDK 6 引入锁升级,在对象头的 Mark Word 里记录锁状态:
无锁 → 偏向锁 → 轻量级锁 → 重量级锁 (只能升,不能降)
| 锁 | 触发条件 | 原理 | 适用场景 |
|---|---|---|---|
| 偏向锁 | 第一个线程访问 | Mark Word 记录线程 ID,之后该线程进入无需 CAS | 单线程反复进入同步块 |
| 轻量级锁 | 多线程交替(无真实竞争) | 用 CAS + 栈帧中的 Lock Record | 持锁时间短、无竞争 |
| 重量级锁 | 真正竞争 | 膨胀为 OS mutex,未抢到的线程阻塞挂起 | 高竞争、持锁时间长 |
💡 中间还有自旋锁:竞争时先"原地转圈"忙等一会儿,赌持锁者很快释放,避免昂贵的内核态挂起。JDK 6+ 自适应自旋(根据历史成功率动态调整)。
⚠️ JDK 15 起偏向锁被废弃(
-XX:+UseBiasedLocking失效)——收益越来越小,维护成本高。面试知道即可。
3. AQS(AbstractQueuedSynchronizer)—— Lock 的基石
ReentrantLock、Semaphore、CountDownLatch、ReentrantReadWriteLock 都基于 AQS。
- 核心两件套:
volatile int state:同步状态(0 无锁,>0 持有/重入次数)。- 一个 CLH 变体 FIFO 双向队列:管理等待线程。
- 工作流:
tryAcquire用 CAS 改 state → 成功拿到锁 → 失败则封装成 Node 入队 →LockSupport.park()挂起 → 前驱唤醒后unpark()。 - 设计模式:模板方法。AQS 定义骨架(入队、挂起、唤醒),子类只实现
tryAcquire/tryRelease(独占)或tryAcquireShared(共享)。 - 公平 vs 非公平:非公平锁新线程直接插队抢(吞吐高),公平锁严格按队列顺序。
4. synchronized vs Lock 对比(必背)
| 维度 | synchronized | Lock(如 ReentrantLock) |
|---|---|---|
| 本质 | JVM 关键字 | JUC 接口/类 |
| 释放锁 | 自动(出作用域/异常) | 必须手动 finally { lock.unlock() } |
| 响应中断 | ❌ | ✅ lockInterruptibly() |
| 超时/尝试获取 | ❌ | ✅ tryLock(time) |
| 公平性 | 仅非公平 | 可选公平/非公平 |
| 条件变量 | 1 个(wait/notify) |
多个 Condition |
| 可重入 | ✅ | ✅ |
口诀:简单同步用
synchronized(自动释放、够用),需要"中断/超时/多条件/公平"时才上Lock。
2.2 volatile
1. 两大语义
- 保证可见性:volatile 变量的写立即对其他线程可见。底层借助 CPU 缓存一致性协议(MESI) + 写屏障把 Store Buffer 刷入主内存,读屏障强制重新加载。
- 禁止指令重排序:插入内存屏障(Memory Barrier)。
2. 不保证原子性 ❗
volatile int i = 0;
i++; // 读-改-写 三步,仍然不安全!
要原子性 → 用 AtomicInteger 或 synchronized。
3. 四种内存屏障
LoadLoad、LoadStore、StoreStore、StoreLoad(最贵,全能屏障)。JMM 在 volatile 写前插 StoreStore、写后插 StoreLoad;volatile 读后插 LoadLoad + LoadStore。
4. happens-before
volatile 写 happens-before 后续对同一变量的 volatile 读 —— 这是可见性的形式化保证。
5. 经典应用:DCL 单例
public class Singleton {
private static volatile Singleton instance; // 必须 volatile!
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
为什么必须 volatile? new Singleton() 在字节码上是三步:
- 分配内存
- 调用构造(初始化)
- 赋值引用给
instance
若 2、3 重排序,另一个线程可能在 instance != null 时拿到未初始化完的对象 → NPE。volatile 禁止重排,消除这个窗口。
2.3 线程池
1. 七大参数
new ThreadPoolExecutor(
int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 非核心线程空闲存活时间
TimeUnit unit, // 时间单位
BlockingQueue<Runnable> workQueue, // 任务队列
ThreadFactory threadFactory, // 线程工厂(命名很有用)
RejectedExecutionHandler handler // 拒绝策略
);
2. 工作流程(顺序是核心!)
提交任务
│
▼
核心线程未满? ──是──▶ 创建【核心线程】执行
│否
▼
任务队列未满? ──是──▶ 放入【队列】等待
│否
▼
未达最大线程数? ──是──▶ 创建【非核心线程】执行
│否
▼
执行【拒绝策略】
🎯 口诀:核心 → 队列 → 最大 → 拒绝。
⚠️ 易错:是先排队再扩容到 max,不是先扩容。这和很多人直觉相反,务必记住。
3. 四种拒绝策略
| 策略 | 行为 |
|---|---|
AbortPolicy(默认) |
抛 RejectedExecutionException |
CallerRunsPolicy |
由提交任务的线程自己跑(天然反压,生产常用) |
DiscardPolicy |
静默丢弃新任务 |
DiscardOldestPolicy |
丢掉队列最老的任务,再重试提交 |
4. ⚠️ 为什么禁用 Executors 快捷方法(阿里规约)
| 方法 | 风险 |
|---|---|
newFixedThreadPool |
队列是 LinkedBlockingQueue(无界)→ OOM |
newSingleThreadExecutor |
同上,无界队列 |
newCachedThreadPool |
max = Integer.MAX_VALUE → 创建过多线程 OOM |
→ 一律用 ThreadPoolExecutor 手动构造,显式指定有界队列 + 合理的线程数。
5. 线程数怎么定
- CPU 密集型:
N + 1(N = CPU 核心数)。 - IO 密集型:
2N,或公式N × (1 + 等待时间/计算时间)。
6. execute vs submit
execute(Runnable) |
submit(Callable/Runnable) |
|
|---|---|---|
| 返回值 | 无 | Future |
| 异常 | 直接抛出 | 封装在 Future,需 future.get() 才暴露 |
2.4 ThreadLocal
1. 作用
给每个线程一份独立副本,实现线程隔离。场景:数据库连接、SimpleDateFormat(非线程安全)、用户 Session/上下文。
2. 原理(关键结构)
Thread ──持有──▶ ThreadLocalMap (threadLocals 字段)
│
└─ Entry[]
key = ThreadLocal 对象 (弱引用 WeakReference)
value = 实际值 (强引用)
threadLocal.set(v) 实际是写到当前线程的 ThreadLocalMap 里,跟其他线程完全隔离。
3. 为什么 key 用弱引用?
防止 ThreadLocal 对象本身泄漏:业务代码里的强引用消失后,key 可被 GC 回收。但注意:这只能防 key 泄漏,防不了 value。
4. 内存泄漏的真正原因
key 被回收 → 变成 null,但 value 仍是强引用。如果线程长期存活(尤其是线程池复用线程),这些 key=null 的 entry 的 value 永久驻留 → 泄漏。
5. 解决办法
ThreadLocal<User> ctx = new ThreadLocal<>();
try {
ctx.set(user);
// 业务逻辑
} finally {
ctx.remove(); // ★ 线程池场景必做!
}
ThreadLocal 在 get/set 时会顺带清理 key=null 的 entry,但别依赖它,线程池里务必手动
remove()。
6. 父子线程传递
- 普通
ThreadLocal:子线程拿不到父线程的值。 InheritableThreadLocal:创建子线程时拷贝一份。- ⚠️ 线程池场景线程被复用,InheritableThreadLocal 会失效/错乱 → 用阿里开源的
TransmittableThreadLocal(TTL)。
第三部分:实战练习
题 1:两个线程交替打印 1~100(三种解法对比)
解法 A:wait/notify(经典)
class Printer {
private int num = 1;
private boolean flag = true; // true:线程1打印,false:线程2打印
public synchronized void print1() throws InterruptedException {
while (num <= 100) {
while (!flag) wait();
if (num > 100) break;
System.out.println(Thread.currentThread().getName() + ": " + num++);
flag = false;
notifyAll();
}
}
public synchronized void print2() throws InterruptedException {
while (num <= 100) {
while (flag) wait();
if (num > 100) break;
System.out.println(Thread.currentThread().getName() + ": " + num++);
flag = true;
notifyAll();
}
}
}
解法 B:Lock + Condition(更精确,推荐)
Lock lock = new ReentrantLock();
Condition c1 = lock.newCondition();
Condition c2 = lock.newCondition();
// 线程1打印完 c2.signal(); 自己 c1.await();
// 线程2打印完 c1.signal(); 自己 c2.await();
解法 C:volatile + 自旋(无锁,简单但费 CPU)
volatile int turn = 1; // 轮到谁
// 线程1: while(turn!=1) ; 打印; turn=2;
三种解法正好串起
synchronized→Lock/Condition→volatile的用法,面试爱问。
题 2:生产者-消费者(阻塞队列版)
BlockingQueue<Integer> queue = new LinkedBlockingQueue<>(10);
// 生产者:queue.put(x); 满了自动阻塞
// 消费者:queue.take(); 空了自动阻塞
一行 put/take 搞定,这就是阻塞队列的价值。
题 3:三个线程顺序打印 A B C(循环 10 次)
思路同题 1 的 Lock + 3 个 Condition,A 唤醒 B、B 唤醒 C、C 唤醒 A,形成环。
附录:知识地图与术语表
知识地图
可见性/有序性 互斥(原子)
volatile ─────────────▶ synchronized / Lock(AQS)
│ │
│单例DCL │锁升级:偏向→轻量→重量
│ │
线程隔离 线程协作
ThreadLocal ─────▶ wait/notify, Condition, BlockingQueue
│remove防泄漏
管理线程 ──▶ 线程池(核心→队列→最大→拒绝)
一图串起基础概念
进程 (资源分配单位)
└─ 线程 (CPU 调度单位, 1:1 内核线程)
├─ run/start 创建与启动
├─ sleep/wait/join 阻塞与协作
├─ interrupt 协作式中断
└─ 协程/虚拟线程 (用户态, 轻量, Java21+)
并发(应对多任务) ──多核──▶ 并行(同时执行)
▲
线程/协程/异步 I/O 都是为了并发
术语速查
| 术语 | 一句话解释 |
|---|---|
| 并发 | 同一时间段内处理多件事(宏观同时) |
| 并行 | 同一时刻真正同时执行(需多核) |
| 临界区 | 访问共享资源的代码段 |
| 竞态条件 | 多线程并发访问共享变量导致结果错误 |
| 死锁 | 线程互相等待对方释放锁,永久阻塞 |
| 活锁 | 线程不断改变状态却无法推进 |
| 饥饿 | 线程长期得不到 CPU/锁 |
| happens-before | 前一操作的结果对后一操作可见的保证 |
| CAS | Compare And Swap,无锁原子操作 |
| AQS | 抽象队列同步器,JUC 锁的基石 |
| JMM | Java 内存模型,规定线程间如何交互 |
| MESI | CPU 缓存一致性协议 |
文档结束。后续可继续补充:JUC 并发工具(CountDownLatch/CyclicBarrier/Semaphore/CompletableFuture)、并发设计模式。
openEuler 是由开放原子开源基金会孵化的全场景开源操作系统项目,面向数字基础设施四大核心场景(服务器、云计算、边缘计算、嵌入式),全面支持 ARM、x86、RISC-V、loongArch、PowerPC、SW-64 等多样性计算架构
更多推荐

所有评论(0)