Files
OneMD/posts/后端开发/java-concurrency-guide.md
2026-06-19 14:45:07 +08:00

272 lines
8.8 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
title: "Java 并发编程完整指南:从线程基础到 JUC 进阶"
date: "2026-05-26"
category: "后端开发"
tags: ["Java", "并发编程", "JUC"]
excerpt: "全面梳理 Java 并发编程知识体系,从线程基础、synchronized 原理到 JUC 并发工具类。"
---
全面梳理 Java 并发编程知识体系,从线程基础、`synchronized` 原理到 JUC 并发工具类,再到线程池最佳实践和虚拟线程。本文配合大量图解和代码示例,帮助你构建完整的并发编程知识框架。
**💡 阅读提示:** 本文适合有一定 Java 基础的开发者。如果你是初学者,建议先阅读 Java 基础语法部分。
## 一、线程基础知识
### 1.1 线程的生命周期
Java 线程在它的生命周期中会经历六种状态,理解这些状态的流转是掌握并发编程的第一步:
- **NEW** — 线程刚创建,尚未调用 `start()`
- **RUNNABLE** — 线程在 JVM 中运行(包括等待 CPU 时间片)
- **BLOCKED** — 线程被阻塞,等待获取锁
- **WAITING** — 无限期等待,需要其他线程显式唤醒
- **TIMED_WAITING** — 超时等待,指定时间后自动返回
- **TERMINATED** — 线程已执行完毕
> 重要区别:BLOCKED 是在等待进入 synchronized 块时发生的,而 WAITING 是通过 Object.wait()、Thread.join() 等方法进入的。
### 1.2 线程的创建方式
Java 中创建线程主要有四种方式,每种都有其适用场景:
#### 继承 Thread 类
```java
class MyThread extends Thread {
@Override
public void run() {
System.out.println("线程运行中: " + Thread.currentThread().getName());
}
}
MyThread thread = new MyThread();
thread.start(); // 启动线程
```
简单直接,但 Java 不支持多继承,灵活性受限。
#### 实现 Runnable 接口
```java
class MyRunnable implements Runnable {
@Override
public void run() {
System.out.println("线程运行中: " + Thread.currentThread().getName());
}
}
Thread thread = new Thread(new MyRunnable());
thread.start();
```
更推荐的方式,任务与线程分离,便于复用。
#### Callable 与 Future
```java
class MyCallable implements Callable<String> {
@Override
public String call() throws Exception {
Thread.sleep(1000);
return "任务完成";
}
}
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<String> future = executor.submit(new MyCallable());
String result = future.get(); // 阻塞获取结果
executor.shutdown();
```
区别于 RunnableCallable 可以有返回值并且可以抛出异常。配合 Future 可以获取异步执行结果。
## 二、synchronized 原理
### 2.1 对象头与 Monitor
Java 中的每个对象都与一个 Monitor 关联。`synchronized` 关键字的底层实现依赖于对象头中的 Mark Word 和 Monitor 机制:
- **Mark Word**:存储对象的 HashCode、GC 分代年龄、锁状态标志等信息
- **Monitor**:一种同步机制,包含 EntryList、WaitSet、Owner 三个关键部分
```java
// synchronized 用法示例
public class Counter {
private int count = 0;
// 1. 同步方法
public synchronized void increment() {
count++;
}
// 2. 同步代码块(更细粒度)
public void incrementWithBlock() {
synchronized (this) {
count++;
}
}
// 3. 静态同步方法(锁的是 Class 对象)
public static synchronized void staticMethod() {
// ...
}
}
```
### 2.2 锁升级过程
JDK 1.6 之后,synchronized 经历了重大优化。锁不再直接膨胀为重量级锁,而是有一个逐步升级的过程:
| 锁状态 | 适用场景 | 实现原理 |
|--------|----------|----------|
| 偏向锁 | 只有一个线程访问同步块 | Mark Word 记录线程 ID,下次同一线程进入无需 CAS |
| 轻量级锁 | 多线程交替执行 | CAS 操作 + 自旋,避免线程阻塞 |
| 重量级锁 | 多线程竞争激烈 | 操作系统 Mutex Lock,线程阻塞 |
#### 偏向锁
偏向锁的"偏"字表示偏向于第一个获取它的线程。当锁处于偏向状态时,Mark Word 会记录当前线程 ID。该线程再次进入同步块时,无需任何同步操作。
```java
// 查看偏向锁延迟(默认 4 秒后开启)
// JVM 参数:-XX:BiasedLockingStartupDelay=0
```
#### 轻量级锁
当偏向锁被其他线程访问时,升级为轻量级锁。线程在栈帧中创建 Lock Record,通过 CAS 操作将 Mark Word 替换为指向 Lock Record 的指针。
#### 重量级锁
当自旋超过一定次数(默认 10 次),或自旋线程数超过 CPU 核数一半时,轻量级锁膨胀为重量级锁。此时线程阻塞,进入 Monitor 的 EntryList 等待。
<div class="warn-box"><strong>⚠️ 注意:</strong>锁只能升级不能降级。一旦变为重量级锁,即使竞争消失也不会回退到轻量级锁。</div>
## 三、JUC 并发工具
`java.util.concurrent`(JUC)包提供了丰富的并发工具,远超 `synchronized` 的能力范围。
### 3.1 ReentrantLock
`synchronized` 更灵活的锁实现,支持公平锁、可中断、超时获取等特性:
```java
ReentrantLock lock = new ReentrantLock(true); // 公平锁
lock.lock();
try {
// 临界区代码
} finally {
lock.unlock(); // 必须在 finally 中释放
}
// 尝试获取锁,等待 1 秒
if (lock.tryLock(1, TimeUnit.SECONDS)) {
try {
// 获取成功
} finally {
lock.unlock();
}
}
```
### 3.2 CountDownLatch
允许一个或多个线程等待其他线程完成操作。典型场景:主线程等待多个子任务完成。
```java
CountDownLatch latch = new CountDownLatch(3);
// 创建三个子线程
for (int i = 0; i < 3; i++) {
new Thread(() -> {
// 执行任务...
latch.countDown(); // 计数减 1
}).start();
}
latch.await(); // 主线程等待,直到计数为 0
System.out.println("所有子任务完成");
```
### 3.3 CyclicBarrier
让一组线程到达一个屏障时被阻塞,直到所有线程都到达后屏障才会打开。与 CountDownLatch 的区别在于它可以重复使用。
### 3.4 Semaphore
信号量,用于控制同时访问资源的线程数量。常用于限流场景:
```java
Semaphore semaphore = new Semaphore(5); // 最多 5 个线程同时访问
for (int i = 0; i < 10; i++) {
new Thread(() -> {
try {
semaphore.acquire();
// 执行任务(最多 5 个线程同时执行)
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
semaphore.release();
}
}).start();
}
```
## 四、线程池原理
### 4.1 核心参数
`ThreadPoolExecutor` 是 Java 线程池的核心实现,其构造器包含七个参数:
```java
public ThreadPoolExecutor(
int corePoolSize, // 核心线程数
int maximumPoolSize, // 最大线程数
long keepAliveTime, // 空闲线程存活时间
TimeUnit unit, // 时间单位
BlockingQueue<Runnable> workQueue, // 任务队列
ThreadFactory threadFactory, // 线程工厂
RejectedExecutionHandler handler // 拒绝策略
)
```
线程池的工作流程:核心线程 → 任务队列 → 最大线程 → 拒绝策略
### 4.2 拒绝策略
当线程池和任务队列都满了,新提交的任务会被拒绝策略处理:
- **AbortPolicy**(默认)— 抛出 RejectedExecutionException
- **CallerRunsPolicy** — 由提交任务的线程自己执行
- **DiscardPolicy** — 静默丢弃,不抛异常
- **DiscardOldestPolicy** — 丢弃队列中最早的未处理任务
> 生产环境最佳实践:不要使用 Executors 的快捷方法创建线程池,而应该手动使用 ThreadPoolExecutor 构造器,这样可以更清楚地了解线程池的配置参数。
## 五、虚拟线程
Java 21 正式引入了虚拟线程(Virtual Threads),这是 Project Loom 的核心成果。虚拟线程是 JVM 管理的轻量级线程,由平台线程(OS 线程)承载执行。
```java
// 创建虚拟线程
Thread vThread = Thread.ofVirtual()
.name("virtual-thread-1")
.start(() -> {
System.out.println("虚拟线程运行中");
});
// 使用 ExecutorService
try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {
executor.submit(() -> {
// 处理请求
});
}
```
虚拟线程的优势在于:当你遇到 IO 阻塞时,虚拟线程会被自动"卸载"(unmount),其承载的平台线程可以切换去执行其他虚拟线程。这意味着你可以用极低的成本创建上百万个虚拟线程。
<div class="info-box"><strong>📌 总结:</strong>传统的"一个请求一个线程"模式在虚拟线程诞生后变得不再昂贵。对于 IO 密集型应用,虚拟线程可以显著提升吞吐量,同时保持代码简单、可读。</div>