如果想看简单明了的说明,请直接跳到第七点往后看
一、volatile 的核心原理
1. 内存可见性(Memory Visibility)
-
问题背景:在多核 CPU 架构下,每个线程有自己的工作内存(CPU 缓存),普通变量的修改可能仅存在于工作内存,不会立即同步到主内存,导致其他线程读取到旧值。
-
volatile 的解决方案:
-
写操作:线程修改
volatile
变量时,强制将新值立即刷新到主内存。 -
读操作:线程读取volatile变量时,丢弃工作内存中的缓存值,直接从主内存加载最新值。
-
底层机制:通过 CPU 的缓存一致性协议(如 MESI 协议)实现多核之间的数据同步
-
禁止指令重排序
-
重排序的危害 编译器和 CPU 可能对无依赖的指令重排序,例如:
// 线程1 flag = true; // 写操作 // 线程2 if (flag) { // 读操作System.out.println(data); // data 可能未被初始化 }
若data的初始化与flag写操作重排序,会导致线程 2 读取到未初始化的data。
-
volatile 的解决方案:
-
通过插入内存屏障(Memory Barrier)指令:
-
写操作屏障:
StoreStore
(写前屏障) +StoreLoad
(写后屏障)。 -
读操作屏障:LoadLoad(读前屏障) +LoadStore(读后屏障)。
-
-
确保:
-
volatile 写之前的操作不会被重排到写之后。
-
volatile 读之后的操作不会被重排到读之前
-
二、volatile 的典型用法
1. 状态标志(最常用场景)
控制线程执行状态,无需加锁:
public class TaskRunner {private volatile boolean stopped = false; // 状态标志public void run() {while (!stopped) { // 线程安全地检测状态// 执行任务}}public void stop() {stopped = true; // 修改后对所有线程立即可见}
}
优势:轻量高效,避免 synchronized
的阻塞开销。
2. 单例模式(双重检查锁定 - DCL)
解决因指令重排序导致的未初始化对象被访问问题:
public class Singleton {private static volatile Singleton instance; // volatile 修饰public static Singleton getInstance() {if (instance == null) {synchronized (Singleton.class) {if (instance == null) {instance = new Singleton(); // 禁止重排序}}}return instance;}
}
原理:volatile
禁止 new Singleton()
的分解步骤重排序(分配内存→初始化对象→赋值引用),避免返回未初始化的对象
3. 一次性发布(One-time Safe Publication)
确保对象初始化完成后才对其他线程可见:
class ResourceHolder {private volatile Resource resource;public Resource getResource() {if (resource == null) {synchronized (this) {if (resource == null) {resource = new Resource(); // 安全发布}}}return resource;}
}
三、volatile 的局限性
1. 不保证原子性
-
复合操作(如
i++
)非原子:i++实际包含三步:读取 i → 加 1 → 写入 i。多线程同时执行可能导致丢失更新。
-
解决方案:
-
使用
synchronized
或ReentrantLock
加锁。 -
使用AtomicInteger等原子类(基于 CAS 实现)。
-
2. 不替代锁机制
-
适用场景有限:仅适用于独立变量的状态同步,无法解决以下问题:
-
多变量的复合操作(如
if (a && b)
)。 -
线程协作(如等待条件满足)。
-
四、volatile vs synchronized
特性 | volatile | synchronized |
---|---|---|
可见性 | ✅ 强制主内存读写 | ✅ 锁释放时刷新主内存 |
有序性 | ✅ 通过内存屏障禁止重排序 | ✅ 锁内操作串行化(临界区内有序) |
原子性 | ❌ 仅保证单次读写原子性 | ✅ 临界区内操作原子 |
阻塞机制 | ❌ 无阻塞 | ✅ 未获锁的线程阻塞 |
适用粒度 | 变量级别 | 代码块/方法级别 |
性能开销 | 低(无上下文切换) | 较高(锁竞争时需切换线程) |
五、最佳实践建议
-
优先作为状态标志:简单开关控制场景首选
volatile
,而非重量级锁。 -
DCL 单例必须使用:避免指令重排序导致对象未初始化完成就被使用。
-
避免误用复合操作:自增、条件判断等需配合原子类或锁。
-
与 final 结合:声明volatile final常量,确保不可变且可见。
-
性能敏感场景验证:高频读写时测试
volatile
与无竞争锁的性能差异(通常volatile
更快)。
六、代码示例:volatile 可见性验证
public class VisibilityDemo {private static volatile boolean flag = true; // 测试时移除 volatile 观察差异
public static void main(String[] args) throws InterruptedException {new Thread(() -> {while (flag) {} // 循环直到 flag 变为 falseSystem.out.println("Thread stopped.");}).start();
Thread.sleep(1000);flag = false; // 主线程修改值}
}
结果:
-
有
volatile
:1 秒后子线程退出。 -
无
volatile
:子线程可能永远无法感知flag
变化(死循环)
七、ThreadLocal 的本质(一句话概括)
每个线程自带一个“私有储物柜”(ThreadLocalMap),ThreadLocal 是打开这个储物柜的钥匙🔑。
-
你(线程)通过钥匙(ThreadLocal对象)存/取自己柜子里的东西(变量值)
-
别人(其他线程)用同样的钥匙只能打开自己的柜子,拿不到你的东西
// 创建一把“储物柜钥匙”(ThreadLocal对象)
private static ThreadLocal<String> userToken = new ThreadLocal<>();
// 线程A存数据:打开自己的柜子,放入令牌
userToken.set("Token_A");
// 线程B存数据:打开自己的柜子,放入令牌
userToken.set("Token_B");
// 线程A取数据:只能拿到自己的Token_A
System.out.println(userToken.get()); // 输出 Token_A
八、没有 ThreadLocal 会怎样?
假设多线程共享同一个变量:
public class SharedDemo {private static String userToken; // 共享变量
public static void main(String[] args) {new Thread(() -> {userToken = "Token_A"; // 线程A设置值System.out.println("A读取: " + userToken); // 可能输出 Token_A}).start();
new Thread(() -> {userToken = "Token_B"; // 线程B覆盖值!System.out.println("B读取: " + userToken); // 输出 Token_B}).start();}
}
会发生:
-
数据互相覆盖
-
线程B 修改userToken后,线程A 的值被意外覆盖
-
-
读取到脏数据
-
线程A 可能刚设置完值,就被线程B 修改,后续操作读到错误数据
-
-
线程不安全
-
多线程竞争同一变量导致逻辑混乱(如:用户A 看到用户B 的信息)
-
九、ThreadLocal 如何解决问题?
1. 数据隔离
-
每个线程通过ThreadLocal对象操作自己独立的副本
private static ThreadLocal<String> userToken = ThreadLocal.withInitial(() -> "default");
-
线程A 操作userToken时,实际访问线程A 自己的存储空间,线程B 完全不受影响。
2. 避免显式传参
-
例如:Web 请求中获取当前用户身份
-
不用 ThreadLocal:需在每个方法参数中传递用户ID
-
用 ThreadLocal:在拦截器存入用户ID,后续方法直接userToken.get()获取
-
// 登录拦截器中
userToken.set(currentUserId);
// 业务方法中直接使用
String id = userToken.get(); // 无需传参!
3. 替代同步锁(特定场景)
-
对共享变量加锁(synchronized)会阻塞线程,降低性能
-
ThreadLocal 无锁操作,用空间换时间,适合高频读写的线程私有数据
十、有 vs 无 ThreadLocal 的对比
场景 | 没有 ThreadLocal | 有 ThreadLocal |
---|---|---|
多线程操作同一变量 | 数据互相污染、覆盖 | 每个线程操作自己的副本,互不干扰 |
跨方法传递参数 | 需显式传递参数,代码耦合度高 | 隐式传递,代码简洁无侵入 |
性能开销 | 需加锁(synchronized),阻塞线程影响性能 | 无锁操作,仅内存占用略高 |
典型应用场景 | 无竞争或低频修改的共享资源 | 线程私有数据(用户会话、数据库连接) |