这么来理解volatile,稳拿offer!

大家好,我是徒手敲代码,今天来分享一下 volatile 这个关键字。

本文将多线程场景,比喻成在饭店里,服务员上菜,以及修改菜单的场景,将抽象的东西实例化。

在计算机中,假设 CPU 是一个精明的老板,它有多个服务员(线程),而内存则是一本巨大的菜单,记载着各种菜肴(数据)。为了提高效率,CPU还为每个服务员配备了私人备忘录(高速缓存),让他们快速查阅更新菜单信息。然而,这种看似牛逼的机制,却悄悄埋下了线程不安全的隐患。

多核CPU中的线程不安全

在多核CPU的舞台上,每个服务员都拥有自己的专属厨房(核心),可以独立烹饪菜肴。当一个服务员在自己的厨房里更新了某道菜的价格,如果没有及时通知其他服务员,他们可能还在用旧的价格为客人下单。这就是线程不安全问题的源头:每个线程之间的通信延迟,导致了数据的不一致。

Java中的线程不安全

当 Java 线程修改某个变量时,它首先在自己的工作内存中进行操作,就像服务员在私人备忘录上涂改菜价。然而,如果不采取措施,这些改动可能不会立刻同步到全局菜单(主内存,即Java堆内存),导致其他线程看到的仍是过期信息。这跟多核CPU中,高速缓存可能导致的数据不一致问题,是同一个道理。

可见性

"可见性"好比服务员间关于菜单更新的默契约定。当一个服务员修改了菜价,其他服务员应当立即知晓并使用新价格。在 Java 中,volatile 关键字就是实现这种"可见性"的秘密武器。它确保了一个线程对volatile变量的修改,对其他线程来说是立即可见的,仿佛服务员之间通过无线电广播实时通报菜价变动。"牛肉饭快买完了,改成五十蚊一份!Over Over !!"

volatile保证变量可见性的原理

在处理器的世界里,内存被划分为不同的区域,如寄存器、高速缓存、主内存等。volatile变量牛逼的地方在于,它能迫使 CPU 遵循以下这些特定的规则:

  • 写入规则:当一个线程修改 volatile 变量时,不仅更新自己的高速缓存,还要立即将新值写回主内存,就像服务员不仅在私人备忘录上改价,还要在公共菜单上正式公布。
  • 读取规则:当另一个线程访问 volatile 变量时,它必须从主内存中获取最新值,而非依赖自己高速缓存的副本。这就如同服务员在接单前,先确认公共菜单上的最新价格。

缓存一致性协议

处理器内部的缓存一致性协议,是 volatile 走上人生巅峰的幕后功臣。这些协议确保了当一个缓存中的数据被修改并写回主内存时,其他缓存中对应的旧数据会被标记为无效 ,迫使其他线程下次访问时从主内存重新加载。这就如同饭店的无线电系统自动通知所有服务员,他们的私人备忘录关于某道菜的价格已失效,需查阅公共菜单获取最新信息。

加了volatile 就万事大吉了吗?

未免想得太简单了吧?如果真是这样,那还要程序猿干嘛呢? 尽管 volatile 保证了可见性,但线程安全问题仍未彻底解决。这是因为 CPU 为了优化性能,有时候会给你来一个指令重排序。就好比服务员在处理订单时,先给客人上饮料(操作A),再记账(操作B),虽然不影响最终结果,但如果另一个服务员恰好此时查看账目,可能会看到未完成的记录。以下 Java 代码演示了这种情况:

arduino 复制代码
volatile boolean ready = false;
int result = 0;

// 线程1
public void writer() {
    // 操作A
    result = 1; 
    // 操作B
    ready = true; 
}

// 线程2
public void reader() {
    // 循环等待 ready 变为 true
    while (!ready); 
    // 假设此时输出为 0,则存在线程安全问题
    System.out.println(result); 
}

尽管ready被声明为volatile,但由于 CPU 可能对操作A和B进行了重排序,线程2可能在ready变真之前看到未初始化的result,导致输出错误结果。虽然重排序并未改变单个变量的值,但破坏了操作的逻辑顺序,引发了线程安全问题。

原子性问题

volatile 虽神通广大,却无法保证一个变量的线程安全。原因在于,Java中的运算并非原子操作。例如以下这行代码,其实包含读取b、加1、写入a三个步骤。

ini 复制代码
int a = b + 1;

在多线程环境下,如果多个线程同时执行这段代码,可能会导致最终结果计数少了。这就像服务员同时给同一道菜计数,一人加1后还没写入菜单,另一人又开始加1,结果只增加了1次销量。

下面这段小程序,就可以模拟上述这个场景。

ini 复制代码
class VolatileCounter {
    volatile int count = 0;

    public void increment() {
        count++;
    }
}

public class Main {
    public static void main(String[] args) throws InterruptedException {
        VolatileCounter counter = new VolatileCounter();
        Thread[] threads = new Thread[100];

        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(() -> {
                for (int j = 0; j < 10000; j++) {
                    counter.increment();
                }
            });
            threads[i].start();
        }

        for (Thread t : threads) {
            t.join();
        }

        System.out.println("Expected count: " + (threads.length * 10000));
        System.out.println("Actual count: " + counter.count);
    }
}

运行这段代码,你会发现实际计数值往往小于预期,证明 volatile 无法保证increment()方法的线程安全性。

volatile什么时候能保证线程安全?

那么,何时volatile能确保线程安全呢?答案是:当仅需要保证变量的可见性,且该变量的读写操作天然具备原子性时。

用单例模式举个例子。

csharp 复制代码
public class Singleton {
    private volatile static Singleton instance; // 使用volatile修饰实例

    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) { // 第一次检查
            synchronized (Singleton.class) {
                if (instance == null) { // 第二次检查
                    instance = new Singleton(); // 创建单例实例
                }
            }
        }
        return instance;
    }
}

在这个例子当中:第一,我们需要所有线程都能看到instance变量被正确赋值;第二,对引用类型(如Singleton实例的引用)的读写操作本身是原子性的。因此可以保证线程安全。

今天的分享到这里结束了,如果你喜欢这种分享知识的方式,可以在下方留言喔。​


关注公众号"徒手敲代码",让知识变得简单。

回复"电子书",获取大佬推荐的Java书籍💪

相关推荐
乐悠小码4 分钟前
数据结构------队列(Java语言描述)
java·开发语言·数据结构·链表·队列
史努比.6 分钟前
Pod控制器
java·开发语言
2的n次方_8 分钟前
二维费用背包问题
java·算法·动态规划
皮皮林5518 分钟前
警惕!List.of() vs Arrays.asList():这些隐藏差异可能让你的代码崩溃!
java
莳光.9 分钟前
122、java的LambdaQueryWapper的条件拼接实现数据sql中and (column1 =1 or column1 is null)
java·mybatis
程序猿麦小七14 分钟前
基于springboot的景区网页设计与实现
java·spring boot·后端·旅游·景区
weisian15120 分钟前
认证鉴权框架SpringSecurity-2--重点组件和过滤器链篇
java·安全
蓝田~22 分钟前
SpringBoot-自定义注解,拦截器
java·spring boot·后端
theLuckyLong23 分钟前
SpringBoot后端解决跨域问题
spring boot·后端·python
.生产的驴24 分钟前
SpringCloud Gateway网关路由配置 接口统一 登录验证 权限校验 路由属性
java·spring boot·后端·spring·spring cloud·gateway·rabbitmq