本篇将深入分析 Java 并发模型的核心内容,包括线程模型、可见性、原子性与有序性问题,并结合 volatile
、synchronized、Happens-Before 规则展开源码与应用层解读。
并发(Concurrency)和并行(Parallelism)是计算机科学中容易混淆但本质不同的两个概念,它们的区别主要体现在任务执行的方式和底层资源分配上。
对比维度 | 并发 | 并行 |
---|---|---|
资源需求 | 单核即可实现 | 需要多核/多 CPU |
执行方式 | 交替执行(逻辑上的“同时”) | 同时执行(物理上的“同时”) |
核心目标 | 高效利用资源(如处理阻塞) | 加速任务完成(如大规模计算) |
典型应用场景 | Web 服务器处理多请求、UI 响应 | 科学计算、图像渲染、大数据处理 |
理解这一区别有助于选择合适的技术(如并发编程用协程,并行计算用多进程)并优化系统性能。
Java 通过 Thread
类或实现 Runnable
/Callable
接口创建线程,其生命周期包含以下状态:
(注:图片暂时省略)
方法名 | 作用描述 | 注意事项 |
---|---|---|
start() |
启动新线程,JVM 调用其 run() 方法 |
多次调用会抛出 IllegalThreadStateException |
run() |
定义线程执行逻辑 | 直接调用 run() 不会创建新线程,仅在当前线程执行 |
join() |
等待线程终止 | 可设置超时时间(如 join(1000) ) |
sleep() |
线程休眠指定时间(不释放锁) | 时间单位为毫秒/纳秒,需处理 InterruptedException |
// 方式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)
主内存(Main Memory
):所有共享变量的存储区域
工作内存(Working Memory
):线程私有,缓存主内存的副本
i++
操作非原子,多线程并发时结果错误volatile
修饰核心特性
可见性:强制线程从主内存读取最新值,修改后立即写回主内存
禁止指令重排序:通过内存屏障实现
底层机制
内存屏障(Memory Barrier)
写操作后插入 StoreLoad
屏障,强制刷新到主内存
读操作前插入LoadLoad
屏障,禁止与后续读操作重排序
MESI 缓存一致性协议
使用场景与限制
// 典型场景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++
仍需同步)
过度使用可能降低性能
底层实现
Monitor 机制
每个对象关联一个 Monitor,通过 monitorenter
和 monitorexit
指令实现锁获取/释放
对象头结构
对象头
(注:图片暂时省略)
锁升级过程
锁类型 | 触发条件 | 特点 |
---|---|---|
偏向锁 | 单线程重复访问同步块 | 通过对象头记录线程ID,减少 CAS 操作 |
轻量级锁 | 多个线程交替执行(无竞争) | 通过自旋(CAS)尝试获取锁 |
重量级锁 | 多线程竞争激烈(自旋超过阈值) | 线程阻塞,依赖操作系统互斥量(Mutex) |
减少同步代码块范围(如同步方法改为同步代码块)
避免在循环内使用同步
优先使用 java.util.concurrent
工具类(如 ReentrantLock
)
规则名称 | 描述 | 代码示例 |
---|---|---|
程序次序规则 | 单线程内操作按代码顺序执行 | 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(); → 主线程可见子线程的所有修改 |
跨线程操作可见性保证:如通过 synchronized 或 volatile 确保修改可见
七、并发常见问题 QA
Q1:为什么在多线程下变量更新线程不可见?
✅ 答案:
由于 JMM 的工作内存机制,线程修改共享变量后:
未及时刷新到主内存
其他线程未从主内存重新加载
解决方案:
使用 volatile 修饰变量
通过 synchronized 同步代码块
Q2:synchronized 和 volatile 有什么区别?
✅ 答案:
对比维度 | synchronized | volatile |
---|---|---|
原子性 | 保证 | 不保证(如 count++) |
可见性 | 保证 | 保证 |
互斥性 | 支持(独占访问) | 不支持 |
性能开销 | 较高 (涉及锁升级) | 较低 |
适用场景 | 复杂同步逻辑(如转账) | 状态标志、双重检查锁定 |
Q3:如何避免死锁?
✅ 答案:
顺序加锁:所有线程按相同顺序获取锁
超时机制:使用 tryLock() 设置超时时间
死锁检测:通过工具(如 jstack)分析线程栈
提示:理解 Java 并发模型需结合理论与实践,建议通过调试工具(如 JConsole、VisualVM)观察线程与锁的状态。