为什么需要多线程?——让程序"三头六臂"
突破单核性能瓶颈
场景类比:快递仓库分拣流水线
单线程就像1个分拣工在1000㎡仓库奔走,多线程则是10个分拣工分区作业,效率提升10倍
现实开发中的典型需求
| 场景类型 | 典型案例 | 多线程价值 |
|---|---|---|
| 异步任务处理 | 聊天软件收发消息 | 避免界面卡死 |
| 高并发服务 | 12306售票系统 | 每秒处理10万级请求 |
| 计算密集型任务 | 视频转码/科学计算 | 利用多核CPU并行计算 |
线程不安全现场直击——银行账户惨案
事故代码还原
public class UnsafeBank {
private int balance = 1000; // 初始金额
// 致命漏洞:非原子操作
public void withdraw(int amount) {
if (balance >= amount) {
// 此处可能发生线程切换!
balance = balance - amount;
}
}
}
事故时间线推演
sequenceDiagram
线程A->>+余额: 读取balance=1000
线程B->>+余额: 读取balance=1000
线程A->>余额: 写入balance=200(扣800)
线程B->>余额: 写入balance=700(扣300)
最终余额-->>结果: 实际扣款1100,余额却剩700!
并发问题的三大元凶
可见性问题——"各自为政的备忘录"
现实比喻:办公室团队协作
小明修改了共享文档,但小红看到的还是本地缓存的老版本
// 没有volatile声明
boolean flag = true;
void threadA() {
while(flag) { /* 可能永远循环 */ }
}
void threadB() {
flag = false;
}
原子性问题——"被打断的取钱操作"
关键原理:
看似简单的balance -= amount在字节码层面是多个操作:
- getstatic #2 // 读取balance
- iload_1 // 加载amount
- isub // 做减法
- putstatic #2 // 写回balance
有序性问题——"编译器的善意优化"
// 可能被重排序为:先分配内存,再初始化对象
instance = new Singleton();
// 实际期望的执行顺序:
1. 分配内存空间
2. 初始化对象
3. 设置instance指向内存地址
一、线程运行机制揭秘
1.1 线程的微观世界
现代CPU通过时间片轮转实现并发执行:
// 示例:线程上下文切换过程
Thread t1 = new Thread(() -> {
while(true) {
// 任务A片段(时间片用完时保存现场)
}
});
Thread t2 = new Thread(() -> {
while(true) {
// 任务B片段(加载上次保存的上下文)
}
});
- 每个时间片约5-100ms
- 上下文切换涉及:程序计数器、寄存器状态、栈指针
1.2 Java内存模型(JMM)
sequenceDiagram
participant 主内存
participant 工作内存A
participant 工作内存B
主内存->>工作内存A: 变量读取(read)
工作内存A->>主内存: 变量写入(write)
主内存->>工作内存B: 变量同步(同步延迟导致可见性问题)
关键特性:
- 原子性:synchronized/Lock保证
- 可见性:volatile/Memory Barrier保证
- 有序性:happens-before原则
二、线程的本质:轻量级执行单元
线程是操作系统调度的最小执行单位,可以理解为一条代码执行的路径。Java中的线程通过Thread类和Runnable接口实现,底层依赖操作系统提供的线程模型。
类比理解
想象一个快递仓库:
- 进程 = 整个仓库(独立资源空间)
- 线程 = 仓库中的分拣工人(共享仓库资源,各自独立工作)
三、线程同步全景图
3.1 锁的进化史
gantt
title 锁升级过程
dateFormat YYYY-MM-DD
section 无锁
初始状态 : 2025-03-01, 1d
section 偏向锁
单线程访问 : 2025-03-02, 2d
section 轻量级锁
少量竞争 : 2025-03-04, 4d
section 重量级锁
激烈竞争 : 2025-03-08, 6d
3.2 锁优化策略
- 自旋锁(默认10次自旋)
- 锁消除(逃逸分析)
- 锁粗化(合并相邻同步块)
四、线程池深度调优
4.1 参数配置矩阵
// 最佳实践示例
ExecutorService executor = new ThreadPoolExecutor(
4, // 核心线程数(CPU密集型建议N+1)
16, // 最大线程数(IO密集型建议2N)
30, // 空闲时间(单位需配合时间单位)
TimeUnit.SECONDS,
new LinkedBlockingQueue<>(1000), // 队列容量根据业务设置
new CustomRejectedPolicy() // 自定义拒绝策略
);
配置公式:
- CPU密集型:coreSize = CPU核心数 + 1
- IO密集型:coreSize = CPU核心数 * 2
五、线程生命周期(流程图解析)
stateDiagram-v2
[*] --> New
New --> Runnable: start()
Runnable --> Running: 获取CPU时间片
Running --> Terminated: 执行完成
Running --> Runnable: yield()
Running --> Blocked: wait()/lock
Blocked --> Runnable: notify()/资源就绪
Running --> TimedWaiting: sleep(1000)
TimedWaiting --> Runnable: 时间到期
关键状态说明:
- 新建:
new Thread()但未启动 - 就绪:等待CPU分配时间片
- 运行:正在执行任务
- 阻塞:等待I/O、锁等资源
- 终止:执行完成或被中断
六、线程同步的三层防护体系
6.1 硬件级保障
- CAS指令:Compare-And-Swap原子操作
CAS魔法——"无锁的原子操作"
AtomicInteger balance = new AtomicInteger(1000);
public void safeWithdraw(int amount) {
balance.updateAndGet(current ->
current >= amount ? current - amount : current
);
}
- 内存屏障:禁止指令重排序
内存屏障——"禁止插队的告示牌"
// volatile保证可见性与禁止重排序
class Singleton {
private volatile static Singleton instance;
public static Singleton getInstance() {
if (instance == null) { // 第一次检查
synchronized(Singleton.class) { // 加锁
if (instance == null) { // 第二次检查
instance = new Singleton();
}
}
}
return instance;
}
}
- 缓存一致性协议:MESI协议
同步锁机制——"会议室使用规则"
// 同步方法
public synchronized void safeWithdraw(int amount) { ... }
// 同步代码块
private final Object lock = new Object();
public void safeWithdraw(int amount) {
synchronized(lock) {
// ...
}
}
6.2 JVM级同步
public class SyncExample {
// 对象头Mark Word结构
// | 锁标志位 | 偏向锁位 | 锁状态 |
// |---------|---------|-------|
// | 01 | 0 | 无锁 |
public synchronized void methodA() {
// 同步代码块
}
}
锁升级过程: 无锁 → 偏向锁 → 轻量级锁 → 重量级锁
6.3 并发工具三剑客
| 工具类 | 适用场景 | 特点 |
|---|---|---|
| CountDownLatch | 多线程任务汇总 | 一次性屏障,不可重置 |
| CyclicBarrier | 多阶段任务控制 | 可重复使用,支持回调 |
| Semaphore | 资源池控制 | 支持公平/非公平模式 |
七、线程同步三大武器
7-1. synchronized关键字
public class Counter {
private int count;
public synchronized void increment() {
count++;
}
}
- 通过对象监视器(Monitor)实现
- 支持方法级和代码块级同步
7-2. Lock接口
Lock lock = new ReentrantLock();
public void doSomething() {
lock.lock();
try {
// 临界区代码
} finally {
lock.unlock();
}
}
- 更灵活的锁机制
- 支持公平锁、超时锁等特性
7-3. volatile关键字
private volatile boolean flag = true;
- 保证变量可见性
- 禁止指令重排序
- 不保证原子性(适合状态标志位)
八、线程池工作原理
8.1 核心参数矩阵
mindmap
root((线程池参数))
核心配置
corePoolSize
maximumPoolSize
keepAliveTime
任务队列
ArrayBlockingQueue
LinkedBlockingQueue
SynchronousQueue
拒绝策略
AbortPolicy
CallerRunsPolicy
DiscardOldestPolicy
8.2 任务处理流程图解
flowchart TD
A[提交任务] --> B{核心线程<br>未满?}
B -->|是| C[创建新线程执行]
B -->|否| D{队列<br>未满?}
D -->|是| E[加入等待队列]
D -->|否| F{最大线程<br>未满?}
F -->|是| G[创建临时线程]
F -->|否| H[执行拒绝策略]
E --> I[队列线程等待执行]
C & G --> J[执行任务]
关键参数:
- corePoolSize:常驻核心线程数
- workQueue:任务缓存队列
- maximumPoolSize:最大线程数
- RejectedExecutionHandler:拒绝策略
九、高频问题解析
9-1. 线程 vs 进程
| 进程 | 线程 | |
|---|---|---|
| 资源开销 | 大(独立内存空间) | 小(共享内存) |
| 通信方式 | 管道、信号、Socket等 | 共享内存、wait/notify |
| 上下文切换 | 成本高 | 成本低 |
9-2. 死锁检测实战
// 死锁示例代码
public class DeadLockDemo {
static Object lockA = new Object();
static Object lockB = new Object();
public static void main(String[] args) {
new Thread(() -> {
synchronized (lockA) {
try { Thread.sleep(100); }
catch (InterruptedException e) {}
synchronized (lockB) {}
}
}).start();
new Thread(() -> {
synchronized (lockB) {
synchronized (lockA) {}
}
}).start();
}
}
诊断步骤:
jps获取进程IDjstack <pid>分析线程状态- 查找"deadlock"关键词
9-3. 死锁产生条件
- 互斥条件
- 请求与保持
- 不可剥夺
- 循环等待 避免方案:
- 按顺序获取锁
- 使用tryLock()设置超时
- 通过ThreadMXBean检测死锁
9-4. 死锁检测四步法
- 使用jps获取进程ID
jps -l - 生成线程转储
jstack <pid> > thread_dump.log - 分析死锁信息
Found one Java-level deadlock: "Thread-2": waiting to lock monitor 0x00007f8934003f58 (object 0x0000000716c7c6c0) which is held by "Thread-1" - 使用VisualVM可视化分析
9-5. 内存泄漏排查技巧
graph TD
A[内存持续增长] --> B[生成堆转储]
B --> C[使用MAT分析]
C --> D[查找ThreadLocal引用链]
D --> E[定位未清理的Entry]
E --> F[添加remove调用]
9-6. ThreadLocal原理
- 每个线程独立存储空间
- 通过ThreadLocalMap实现
- 典型应用场景:
- 数据库连接管理
- 用户会话信息存储
9-7. ThreadLocal内存泄漏防护
sequenceDiagram
participant App as 应用程序
participant ThreadLocal
participant Thread
participant Entry
App->>ThreadLocal: 创建实例
ThreadLocal->>Thread: 存储数据
Thread->>Entry: 维护ThreadLocalMap
Note over Thread,Entry: Key为弱引用<br>Value为强引用
App->>ThreadLocal: remove()
ThreadLocal->>Entry: 删除对应条目
防护建议:
- 始终在finally块中调用remove()
- 避免使用static修饰ThreadLocal
- 定期检查内存使用情况
十、性能优化
10-1. 线程上下文切换优化
- 案例:某交易系统上下文切换从5000次/秒降到800次/秒
- 优化手段:
- 减少同步代码块粒度
- 使用无锁数据结构
- 调整线程池大小
10-2. CPU利用率提升方案
// 错误示例:过度使用线程
for(int i=0; i<10000; i++){
new Thread(() -> {
// 简单计算任务
}).start();
}
// 优化方案:使用线程池+批量处理
ExecutorService executor = Executors.newFixedThreadPool(8);
List<Future> futures = new ArrayList<>();
for(int i=0; i<10000; i++){
futures.add(executor.submit(() -> {
// 批量处理任务
}));
}
十一、Java线程新特性
11-1. 虚拟线程(协程)
// 预览版示例(JDK19+)
Thread.startVirtualThread(() -> {
System.out.println("轻量级虚拟线程");
});
核心优势:
- 创建成本:传统线程1MB vs 虚拟线程~200B
- 切换效率:用户态切换,无内核介入
11-2. Structured Concurrency
try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
Future<String> user = scope.fork(() -> findUser());
Future<Integer> order = scope.fork(() -> fetchOrder());
scope.join();
return new Response(user.get(), order.get());
}
特点:
- 线程生命周期与代码块绑定
- 异常传播机制完善