关于 volatile 的作用及其原理

27 阅读4分钟

作用

  1. 保证多核CPU中线程的可见性
  2. 禁止指令重排

底层原理

volatile 通过在适当位置插入内存屏障来实现其可见性和有序性保证。


什么是 内存屏障

内存屏障就是一个 CPU 指令,用于控制指令执行顺序和内存操作顺序。由于编译器优化和处理器内部的重排序机制,指令执行顺序可能与程序的逻辑顺序不一致。内存屏障通过抑制这种重排序,确保内存操作按预期顺序执行。

内存屏障类型

  • StoreStore:你先读我再读
  • StoreLoad:你先读我再存,保证你读之前的,不被我存的影响
  • LoadLoad:你先存我再存,不能让你覆盖我 -LoadStore:你读完写完,我再读写,你先处理完我再处理

Jvm 对volatile 的屏障设定的规则

第二个操作第二个操作第二个操作
第一个操作普通 读/写volatile 读volatile 写
普通 读/写NO
volatile 读NONONO
volatile 写NONO
  • 在每个volatile写操作的前面插入一个StoreStore屏障。

  • 在每个volatile写操作的后面插入一个StoreLoad屏障。

  • 在每个volatile读操作的后面插入一个LoadLoad屏障。

  • 在每个volatile读操作的后面插入一个LoadStore屏障。

作者:monkeysayhi
链接:juejin.cn/post/684490…
来源:稀土掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。


什么是线程的可见性

可见性是指一个线程对共享变量变量的修改对其他线程是可见的,并且这种问只会在多核CPU中出现,因为单核CPU使用的都是同一个CPU缓存,因此单核CPU不存在可见性问题。

通常我们通过 volatile 来实现每次强制从主存中读取,但是为什么会有会有线程之间的修改无法可见的这种情况的出现呢?

这是因为 CPU 缓存的存在,比如说下面这个例子,开始 stop 在主存中的值为 false ,Worker 线程拿到后将 stop = false载入CPU1缓存,等到Controller线程 拿到时 stop = false载入CPU2缓存,然后将其改为 stop = true刷新到主存,但是 worker 线程对此没有感知,也就还是继续使用旧值,导致一直死循环。

private boolean stop = false;
private long counter = 0;
@Test
public void test2() throws InterruptedException {
    Thread worker = new Thread(() -> {
        System.out.println("Worker线程开始");
        while (!stop) {
            counter++; // 纯计算,没有IO操作
            // 不使用println,避免同步副作用
        }
        System.out.println("Worker线程结束,counter = " + counter);
    });
    Thread controller = new Thread(() -> {
        try {
            Thread.sleep(2000);
            System.out.println("Controller线程:设置stop = true");
            stop = true; // 修改stop,可能只在缓存中
            System.out.println("Controller线程:设置完成");
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    });
    worker.start();
    controller.start();
    try {
        worker.join(5000); // 等待5秒
        if (worker.isAlive()) {
            System.out.println("❌ Worker线程仍在运行,可见性问题重现!");
            worker.interrupt();
        } else {
            System.out.println("✅ Worker线程正常停止");
        }
    } catch (InterruptedException e) {
        e.printStackTrace();
    }

}
# 加上volatile 前


Worker线程开始
Controller线程:设置stop = true
Controller线程:设置完成
❌ Worker线程仍在运行,可见性问题重现!

# 加上volatile 后

Worker线程开始
Controller线程:设置stop = true
Controller线程:设置完成
Worker线程结束,counter = 5803390503
true
✅ Worker线程正常停止

什么是指令重排

编译器/CPU为了优化性能,改变程序指令的执行顺序,但是在多线程环境下可能会造成很多问题

场景1

在使用单例模式时,线程 A 第一次使用,所以会 new 一个新的 对象,new 操作分为三步

  • 分配内存
  • 初始化对象
  • 将 instance 指向分配的内存

但是由于指令重排,顺序可能变成

  • 分配内存
  • 将 instance 指向分配的内存
  • 初始化对象

此时如果有另一个 线程 B 获取单例的话就会获取到一个不为 null 的索引,但是索引指向的内存却没有初始化好的对象,这就相当于装货员还没把东西放进箱子里你就拿走用了,会造成各种错误

public class CompleteDemo {
    
    static class SingletonWithProblems {
        // 不同类型的字段
        private String name = null;           // 会导致 NPE
        private int value = 0;                // 会导致逻辑错误
        private List<String> list = null;     // 会导致 NPE
        private final long createTime;        // final 字段
        
        public SingletonWithProblems() {
            this.name = "Initialized";
            this.value = 42;
            this.list = new ArrayList<>();
            this.list.add("item1");
            this.createTime = System.currentTimeMillis();
            
            // 增加重排概率
            try {
                Thread.sleep(100);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        
        // 测试方法
        public void testState() {
            System.out.println("=== 对象状态测试 ===");
            System.out.println("name: " + name);
            System.out.println("value: " + value);
            System.out.println("list: " + list);
            System.out.println("createTime: " + createTime);
            
            // 尝试使用字段
            try {
                System.out.println("name.length(): " + name.length());
            } catch (NullPointerException e) {
                System.err.println("❌ NPE in name.length(): " + e.getMessage());
            }
            
            try {
                System.out.println("list.size(): " + list.size());
            } catch (NullPointerException e) {
                System.err.println("❌ NPE in list.size(): " + e.getMessage());
            }
            
            // 逻辑检查
            if (value == 0) {
                System.err.println("❌ 逻辑错误: value 应该是 42,但现在是 0");
            }
        }
    }
    
    private static SingletonWithProblems instance;
    
    public static void runTest() {
        System.out.println("=== 完整问题演示 ===");
        
        final int[] errorCount = {0};
        final int[] testCount = {0};
        
        // 运行多次测试
        for (int i = 0; i < 1000; i++) {
            testCount[0]++;
            instance = null; // 重置
            
            Thread creator = new Thread(() -> {
                instance = new SingletonWithProblems();
            });
            
            Thread tester = new Thread(() -> {
                try {
                    Thread.sleep(50); // 增加问题发生概率
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                
                if (instance != null) {
                    // 检查是否是未初始化的对象
                    if (instance.createTime == 0) { // final 字段默认值
                        errorCount[0]++;
                        System.out.println("❌ 发现未初始化对象!测试 #" + i);
                        instance.testState();
                    }
                }
            });
            
            creator.start();
            tester.start();
            
            try {
                creator.join();
                tester.join();
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
        }
        
        System.out.println("\n=== 测试结果 ===");
        System.out.println("总测试次数: " + testCount[0]);
        System.out.println("发现错误次数: " + errorCount[0]);
        System.out.println("错误率: " + (errorCount[0] * 100.0 / testCount[0]) + "%");
    }
}


volatile 如何防止指令重排

使用 内存屏障,在操作之间设置屏障,保证操作 x 在操作 y 之前进行。