Java 并发模型:线程、锁与内存可见性机制详解
本篇将深入分析 Java 并发模型的核心内容,包括线程模型、可见性、原子性与有序性问题,并结合 volatile
、synchronized、Happens-Before 规则展开源码与应用层解读。
一、并发与并行的区别
并发(Concurrency)和并行(Parallelism)是计算机科学中容易混淆但本质不同的两个概念,它们的区别主要体现在任务执行的方式和底层资源分配上。
1. 核心定义
并发(Concurrency)
- 定义 :多个任务在重叠的时间段内交替执行,但不一定同时。
- 示例:单核 CPU 通过时间片轮转交替处理多个任务,看似"同时"进行,但任一时刻只有一个任务实际执行。
- 目标:提高资源利用率(如避免 CPU 空闲等待 I/O 操作)。
并行(Parallelism)
- 定义:多个任务真正同时执行,需要多核/多 CPU 或分布式系统的支持。
- 示例:多核 CPU 的每个核心独立处理不同任务,实现物理上的同时运行。
- 目标:缩短任务总耗时,提升吞吐量。
2. 对比表格
对比维度 | 并发 | 并行 |
---|---|---|
资源需求 | 单核即可实现 | 需要多核/多 CPU |
执行方式 | 交替执行(逻辑上的"同时") | 同时执行(物理上的"同时") |
核心目标 | 高效利用资源(如处理阻塞) | 加速任务完成(如大规模计算) |
典型应用场景 | Web 服务器处理多请求、UI 响应 | 科学计算、图像渲染、大数据处理 |
3. 生活化举例
并发场景
- 例子 :你一边吃饭一边回消息。
本质:实际是"夹菜→放下筷子→打字→再夹菜"的交替过程,同一时间只做一件事,但通过快速切换高效完成多项任务。
并行场景
- 例子 :你和朋友同时打扫不同房间。
本质:每人独立工作,物理上同时进行,总时间显著缩短。
4. 技术与应用场景
实现并发的技术
- 多线程(单核切换)、协程(Coroutine)、异步编程(Async/Await)。
- 典型场景 :
- 高并发的网络服务器(如 Nginx)。
- 用户界面响应(避免卡顿)。
实现并行的技术
- 多进程(多核分配)、GPU 并行计算、分布式系统(如 Hadoop)。
- 典型场景 :
- 大数据处理(如 Spark)。
- 机器学习训练、3D 渲染。
5. 关键总结
- 并发 是"处理多个任务的能力"(逻辑上同时),并行是"执行多个任务的能力"(物理上同时)。
- 并发解决结构问题:优化任务调度,避免阻塞(如等待 I/O)。
- 并行解决性能问题:通过多核/分布式加速计算。
- 两者可结合:例如多线程程序在多核 CPU 上既并发(线程切换)又并行(多核同时执行)。
理解这一区别有助于选择合适的技术(如并发编程用协程,并行计算用多进程)并优化系统性能。
二、线程的基本概念
1. 线程生命周期与核心方法
Java 通过 Thread
类或实现 Runnable
/Callable
接口创建线程,其生命周期包含以下状态:
(注:图片暂时省略)
关键方法解析
方法名 | 作用描述 | 注意事项 |
---|---|---|
start() |
启动新线程,JVM 调用其 run() 方法 |
多次调用会抛出 IllegalThreadStateException |
run() |
定义线程执行逻辑 | 直接调用 run() 不会创建新线程,仅在当前线程执行 |
join() |
等待线程终止 | 可设置超时时间(如 join(1000) ) |
sleep() |
线程休眠指定时间(不释放锁) | 时间单位为毫秒/纳秒,需处理 InterruptedException |
代码示例:三种创建方式
java
// 方式1:继承 Thread 类
class MyThread extends Thread {
@Override
public void run() {
System.out.println("Thread running");
}
}
// 方式2:实现 Runnable 接口
Runnable task = () -> System.out.println("Runnable running");
new Thread(task).start();
// 方式3:实现 Callable 接口(可返回结果)
ExecutorService executor = Executors.newSingleThreadExecutor();
Future<String> future = executor.submit(() -> "Callable result");
System.out.println(future.get()); // 输出 "Callable result"
三、Java 内存模型(JMM)
- 内存结构模型
-
JMM 定义了多线程环境下内存交互规则: (注:圖片暫時省略)
-
主内存(
Main Memory
):所有共享变量的存储区域 -
工作内存(
Working Memory
):线程私有,缓存主内存的副本
-
- 三大核心问题
问题类型 | 描述 | 示例场景 |
---|---|---|
可见性 | 线程对共享变量的修改对其他线程不可见 | 线程A修改flag 后,线程B仍读取旧值 |
原子性 | 操作被中途打断导致数据不一致 | i++ 操作非原子,多线程并发时结果错误 |
有序性 | 编译器和处理器优化导致指令重排序 | 单例模式双重检查锁需用volatile 修饰 |
四、volatile 的实现原理
-
核心特性
-
可见性:强制线程从主内存读取最新值,修改后立即写回主内存
-
禁止指令重排序:通过内存屏障实现
-
-
底层机制
-
内存屏障(Memory Barrier)
-
写操作后插入
StoreLoad
屏障,强制刷新到主内存 -
读操作前插入
LoadLoad
屏障,禁止与后续读操作重排序
-
-
MESI 缓存一致性协议
- CPU 通过监听总线,使其他核心的缓存行失效(Invalidate)
-
-
使用场景与限制
java
// 典型场景1:状态标志位
volatile boolean shutdownRequested = false;
// 典型场景2:双重检查锁定(Double-Checked Locking)
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;
}
}
限制
-
不保证原子性(如
volatile int count++
仍需同步) -
过度使用可能降低性能
五、synchronized 的实现与优化
-
底层实现
-
Monitor 机制 每个对象关联一个 Monitor,通过
monitorenter
和monitorexit
指令实现锁获取/释放 -
对象头结构 对象头 (注:图片暂时省略)
-
-
锁升级过程
锁类型 | 触发条件 | 特点 |
---|---|---|
偏向锁 | 单线程重复访问同步块 | 通过对象头记录线程ID,减少 CAS 操作 |
轻量级锁 | 多个线程交替执行(无竞争) | 通过自旋(CAS)尝试获取锁 |
重量级锁 | 多线程竞争激烈(自旋超过阈值) | 线程阻塞,依赖操作系统互斥量(Mutex) |
- 优化建议
-
减少同步代码块范围(如同步方法改为同步代码块)
-
避免在循环内使用同步
-
优先使用
java.util.concurrent
工具类(如ReentrantLock
)
六、Happens-Before 原则
- 规则详解
规则名称 | 描述 | 代码示例 |
---|---|---|
程序次序规则 | 单线程内操作按代码顺序执行 | nt a=1; int b=a; (b 的赋值在 a 之后) |
监视器锁规则 | 解锁操作先于后续的加锁操作 | synchronized(lock) { ... } 解锁后,其他线程才能获取锁 |
volatile变量规则 | volatile 写操作先于后续的读操作 | volatile int x=0; 线程A写 x=1 → 线程B读 x 必为1 |
线程启动规则 | Thread.start() 先于该线程的任何操作 | thread.start(); → 新线程中的 run() 方法 |
线程终止规则 | 线程的所有操作先于其他线程检测到其终止 | thread.join(); → 主线程可见子线程的所有修改 |
- 实际应用 解释器与编译器的优化限制:禁止违反 Happens-Before 的指令重排序
跨线程操作可见性保证:如通过 synchronized
或 volatile
确保修改可见
七、并发常见问题 QA
💬 Q1:为什么在多线程下变量更新线程不可见? ✅ 答案: 由于 JMM 的工作内存机制,线程修改共享变量后:
-
未及时刷新到主内存
-
其他线程未从主内存重新加载
解决方案:
-
使用 volatile 修饰变量
-
通过 synchronized 同步代码块
-
💬 Q2:synchronized 和 volatile 有什么区别? ✅ 答案:
对比维度 | synchronized | volatile |
---|---|---|
原子性 | 保证 | 不保证(如 count++ ) |
可见性 | 保证 | 保证 |
互斥性 | 支持(独占访问) | 不支持 |
性能开销 | 较高 (涉及锁升级) | 较低 |
适用场景 | 复杂同步逻辑(如转账) | 状态标志、双重检查锁定 |
💬 Q3:如何避免死锁? ✅ 答案:
- 顺序加锁:所有线程按相同顺序获取锁
- 超时机制:使用
tryLock()
设置超时时间 - 死锁检测:通过工具(如
jstack
)分析线程栈
提示:理解 Java 并发模型需结合理论与实践,建议通过调试工具(如 JConsole、VisualVM)观察线程与锁的状态。