272 lines
8.8 KiB
Markdown
272 lines
8.8 KiB
Markdown
---
|
||
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();
|
||
```
|
||
|
||
区别于 Runnable,Callable 可以有返回值并且可以抛出异常。配合 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>
|
||
|