💡 摘要:你是否曾尝试学习 Java 多线程,却被“线程安全”、“竞态条件”、“内存可见性”等术语搞得一头雾水? 或者写了一个看似正确的多线程程序,却在运行时出现诡异的 bug,难以复现? 问题往往不在于“多线程本身太难”,而在于基础知识不牢固。 在正式踏入
Thread、Runnable、synchronized的世界之前,必须先打好地基。 本文将系统梳理学习 Java 多线程前必须掌握的 6 大核心前置知识:从 JVM 内存模型、CPU 缓存架构,到原子性、可见性、有序性三大特性,再到volatile和synchronized的底层原理。 只有理解了这些“底层逻辑”,你才能真正驾驭多线程编程,写出高效、安全的并发代码。 文末附面试高频问题解析,助你打通任督二脉。
我们习惯于顺序编程:代码从上到下执行,结果可预测。 但多线程打破了这一“直觉”:
// 你以为的:
int a = 0;
a++; // a 变成 1
// 多线程下的现实:
// 两个线程同时执行 a++,结果可能不是 2!❌ 问题根源:
在学习 new Thread() 之前,必须先理解这些“破坏直觉”的底层机制。
Java 内存模型(Java Memory Model, JMM)是理解并发的基石。 它定义了线程如何与内存交互,是 Java 并发编程的“游戏规则”。
// 共享变量
int sharedVar = 0; // 存储在主内存
// 线程操作流程:
// 1. 从主内存读取 sharedVar 到工作内存
// 2. 在工作内存中修改副本
// 3. 将修改后的值写回主内存🔥 关键点:线程不能直接操作主内存,必须通过工作内存“中转”。
特性 | 问题 | 解决方案 |
|---|---|---|
原子性(Atomicity) | a++ 可能被中断 | synchronized, AtomicInteger |
可见性(Visibility) | 线程 A 修改,线程 B 看不到 | volatile, synchronized |
有序性(Ordering) | 代码重排序导致逻辑错误 | volatile, synchronized, final |
✅ 掌握这三大特性,就掌握了并发问题的“病根”与“药方”。
CPU 0: [L1 Cache] → [L2 Cache] → [主内存]
CPU 1: [L1 Cache] → [L2 Cache] → [主内存]⚠️ 问题:线程 A(在 CPU 0)修改变量,线程 B(在 CPU 1)可能读到旧值——可见性问题。
硬件层面通过协议(如 MESI)保证缓存一致性,但:
🔑 结论:不能依赖硬件自动保证“实时可见”,需编程语言层面(如 JMM)提供更强保证。
一种 CPU 指令,用于控制读写操作的顺序,防止重排序。
LoadLoad:确保后续读操作在之前读之后StoreStore:确保后续写操作在之前写之后LoadStore:确保后续写操作在之前读之后StoreLoad:确保后续读操作在之前写之后(最强屏障)✅
volatile关键字的实现就依赖内存屏障。
一个操作要么全部执行,要么完全不执行,不会被线程调度机制打断。
操作 | 是否原子 |
|---|---|
int a = 1; | ✅(32 位以内基本类型) |
long b = 1L; | ❌(64 位,可能分两步写) |
a++ | ❌(读 + 改 + 写,三步) |
volatile long c = 1L; | ✅(volatile 保证 long/double 的原子性) |
synchronized:加锁,确保同一时刻只有一个线程执行java.util.concurrent.atomic 包:如 AtomicInteger,基于 CAS(Compare-And-Swap)实现无锁原子操作AtomicInteger atomicInt = new AtomicInteger(0);
atomicInt.incrementAndGet(); // 原子自增public class VisibilityDemo {
private boolean running = true;
public void run() {
while (running) {
// do work
}
System.out.println("Stopped");
}
public void stop() {
running = false;
}
}❌ 问题:
run(),running 变量被缓存到其工作内存stop(),修改 running = falserunning 的变化,无限循环!volatile 关键字private volatile boolean running = true;synchronizedpublic synchronized void stop() {
running = false;
}
public synchronized void run() {
while (running) { ... }
}monitorenter)和退出(monitorexit)会强制工作内存与主内存同步。为了提高性能,编译器和 CPU 会对指令进行重排序,只要保证单线程语义不变。
int a = 0;
boolean flag = false;
// 原始代码
a = 1; // 1
flag = true; // 2
// 可能被重排序为:
flag = true; // 2
a = 1; // 1⚠️ 多线程下问题:如果另一个线程看到
flag = true,但a还是 0,就会出错。
JMM 定义了先行发生关系,是判断数据是否存在竞争、线程是否安全的基石。
synchronized 的解锁 happens-before 于后续加锁volatile 写 happens-before 于后续 volatile 读Thread.start() happens-before 于线程内任何操作✅ 如果两个操作没有 happens-before 关系,它们就可能被重排序。
volatile 与 synchronized 的底层原理volatile 的实现volatile 变量操作前后插入内存屏障lock 前缀)确保缓存一致性synchronized 的实现monitorenter / monitorexit 指令触发内存同步🔍 理解这些,才能明白为什么
synchronized既能保证原子性,又能保证可见性和有序性。
在学习 Thread 类和并发工具类之前,务必先掌握以下六大核心前置知识:
知识点 | 核心概念 | 关键字/工具 |
|---|---|---|
JVM 内存模型 | 主内存 vs 工作内存 | JMM |
CPU 缓存 | 缓存独立性、内存屏障 | MESI、StoreLoad |
原子性 | 不可分割的操作 | synchronized, AtomicXXX |
可见性 | 修改对其他线程可见 | volatile, synchronized |
有序性 | 防止指令重排序 | volatile, synchronized, final |
happens-before | 内存操作的顺序规则 | 六大规则 |
🚀 学习路径建议:
volatile 和 synchronized 的原理Thread、Runnable、线程池等上层 APIjava.util.concurrent 包地基不牢,地动山摇。只有理解了这些底层机制,你才能真正驾驭 Java 多线程,写出既高效又安全的并发程序。
答: JMM 是 Java 虚拟机规范定义的,关于线程如何与内存交互的模型。 它屏蔽了硬件和操作系统的内存访问差异,确保 Java 程序在各种平台上行为一致。 核心是主内存与工作内存的抽象,以及原子性、可见性、有序性三大特性。
volatile 关键字的作用是什么?答:
volatile 变量的写操作会立即刷新到主内存,读操作会强制从主内存读取;volatile i++ 仍不是原子操作。synchronized 如何保证可见性和原子性?答:
答: happens-before 是 JMM 定义的两个操作之间的偏序关系。 如果 A happens-before B,则 A 的执行结果对 B 可见。 它是判断数据竞争和线程安全的基础。 即使代码重排序,只要 happens-before 关系不变,程序语义就不变。
答: 不一定。 根据 JMM,32 位及以下的基本类型读写是原子的。
long和double是 64 位,可能被分为两个 32 位操作,不是原子的。 但volatile long和volatile double的读写是原子的。
答:
volatile、synchronized 等同步机制建立 happens-before 关系,禁止有害重排序。