欢迎来到尧图网

客户服务 关于我们

您的位置:首页 > 文旅 > 艺术 > 说说volatile的用法及原理。

说说volatile的用法及原理。

2025/6/20 0:27:33 来源:https://blog.csdn.net/chentainshao/article/details/148746891  浏览:    关键词:说说volatile的用法及原理。

如果想看简单明了的说明,请直接跳到第七点往后看

一、volatile 的核心原理

1. 内存可见性(Memory Visibility)
  • 问题背景:在多核 CPU 架构下,每个线程有自己的工作内存(CPU 缓存),普通变量的修改可能仅存在于工作内存,不会立即同步到主内存,导致其他线程读取到旧值。

  • volatile 的解决方案:

    • 写操作:线程修改 volatile 变量时,强制将新值立即刷新到主内存

    • 读操作:线程读取volatile变量时,丢弃工作内存中的缓存值,直接从主内存加载最新值。

    • 底层机制:通过 CPU 的缓存一致性协议(如 MESI 协议)实现多核之间的数据同步

禁止指令重排序

  1. 重排序的危害 编译器和 CPU 可能对无依赖的指令重排序,例如:

    // 线程1
    flag = true;  // 写操作
    // 线程2
    if (flag) {   // 读操作System.out.println(data);  // data 可能未被初始化
    }

    若data的初始化与flag写操作重排序,会导致线程 2 读取到未初始化的data。

  1. 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。多线程同时执行可能导致丢失更新。

  • 解决方案:

    • 使用 synchronizedReentrantLock 加锁。

    • 使用AtomicInteger等原子类(基于 CAS 实现)。

2. 不替代锁机制
  • 适用场景有限:仅适用于独立变量的状态同步,无法解决以下问题:

    • 多变量的复合操作(如 if (a && b))。

    • 线程协作(如等待条件满足)。

四、volatile vs synchronized

特性volatilesynchronized
可见性✅ 强制主内存读写✅ 锁释放时刷新主内存
有序性✅ 通过内存屏障禁止重排序✅ 锁内操作串行化(临界区内有序)
原子性❌ 仅保证单次读写原子性✅ 临界区内操作原子
阻塞机制❌ 无阻塞✅ 未获锁的线程阻塞
适用粒度变量级别代码块/方法级别
性能开销低(无上下文切换)较高(锁竞争时需切换线程)

五、最佳实践建议

  1. 优先作为状态标志:简单开关控制场景首选 volatile,而非重量级锁。

  2. DCL 单例必须使用:避免指令重排序导致对象未初始化完成就被使用。

  3. 避免误用复合操作:自增、条件判断等需配合原子类或锁。

  4. 与 final 结合:声明volatile final常量,确保不可变且可见。

  5. 性能敏感场景验证:高频读写时测试 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();}
}
会发生:
  1. 数据互相覆盖

    • 线程B 修改userToken后,线程A 的值被意外覆盖

  2. 读取到脏数据

    • 线程A 可能刚设置完值,就被线程B 修改,后续操作读到错误数据

  3. 线程不安全

    • 多线程竞争同一变量导致逻辑混乱(如:用户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),阻塞线程影响性能无锁操作,仅内存占用略高
典型应用场景无竞争或低频修改的共享资源线程私有数据(用户会话、数据库连接)

版权声明:

本网仅为发布的内容提供存储空间,不对发表、转载的内容提供任何形式的保证。凡本网注明“来源:XXX网络”的作品,均转载自其它媒体,著作权归作者所有,商业转载请联系作者获得授权,非商业转载请注明出处。

我们尊重并感谢每一位作者,均已注明文章来源和作者。如因作品内容、版权或其它问题,请及时与我们联系,联系邮箱:809451989@qq.com,投稿邮箱:809451989@qq.com

热搜词