侧边栏壁纸
  • 累计撰写 10 篇文章
  • 累计创建 5 个标签
  • 累计收到 2 条评论
标签搜索

目 录CONTENT

文章目录

java volatile 关键字的真正含义

volatile

talk is cheap, show the code

public class NotAtomicVolatile {

    private volatile int a = 0;

    public void increment() {
        a++; // 非原子操作
    }

    public int getCounter() {
        return a;
    }

    public static void main(String[] args) throws InterruptedException {
        NotAtomicVolatile example = new NotAtomicVolatile();
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                example.increment();
            }
        });
        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                example.increment();
            }
        });

        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();

        // expect 2000
        System.out.println("Final a value: " + example.getCounter());
    }
}

上面的示例期待最终的结果为 2000,但是实际上不一定能够得到 2000,大部分情况下结果会小于 2000

为什么?

volatile 关键字主要是为多线程场景服务的,如果我使用了它但是依然无法保证多线程运行的结果,那么我为什么需要它?

volatile 关键字的特性之一为:保证内存可见性但是不保证操作的原子性

这个特性是导致出现预料之外结果的原因,那么到底什么意思?

内存可见性和原子性

要理解 volatile,必须要了解 java 的内存模型(JMM)

java 内存模型 JMM 是一种抽象层模型,它屏蔽了虚拟机在不同平台的具体实现,所以理解这个抽象层就能理解 volatile

在 JMM 中,存在 主内存线程本地缓存 两种重要概念

jmm1

由于没有互斥属性,线程是同时执行的,但是 volatile 修饰的变量 JMM 会保证

  1. 读取时从主内存获取最新值
  2. 写入时直接更新到主内存中

保证了最新数据对所有线程可见,这就是其所指的 内存可见性

但是仅针对于 “单个操作”,例如 读/写。对于复合操作,虽然遵循 单个操作内存可见性,但是无法保证结果准确,因为多个线程可能会交错执行复合操作

再来看最初的示例

public class NotAtomicVolatile {

    private volatile int a = 0;

    public void increment() {
        a++; // 非原子操作
    }

    public int getCounter() {
        return a;
    }

    public static void main(String[] args) throws InterruptedException {
        NotAtomicVolatile example = new NotAtomicVolatile();
        Thread thread1 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                example.increment();
            }
        });
        Thread thread2 = new Thread(() -> {
            for (int i = 0; i < 1000; i++) {
                example.increment();
            }
        });

        thread1.start();
        thread2.start();
        thread1.join();
        thread2.join();

        // expect 2000
        System.out.println("Final a value: " + example.getCounter());
    }
}

每次自增操作实际上都有三步,这就是 volatile 无法控制的复合操作

  1. 从主内存获取 a 的值
  2. 值 +1
  3. 新的值写回主内存

在 JMM 中分析其行为

jmm3-1742122219576

由于两个线程不互斥,同时运行,那么在每一次循环中都有可能出现

  1. 线程 A:读取 a,值为 0
  2. 线程 B:读取 a,值为 0(因为线程 A 还没来得及写回)
  3. 线程 A:将 0 加 1,得到 1
  4. 线程 B:将 0 加 1,得到 1
  5. 线程 A:将 1 写回 a
  6. 线程 B:将 1 写回 a

最终此次循环结束时 a 的值还是 1,如此往复,导致了最终的结果不会是 2000

这就是 volatile 保证内存可见性,不保证操作原子性 的含义,以及可能会导致的后果

如果能保证多个线程中不会存在这个复合操作,那么可以使用它,因为相对于锁的方式,它更加轻量化

具体场景比如 多线程中需要共享状态标志、一写多读场景、以及下面的双重检查单例模式
下面的示例同时也展现了 volatile 的另一个特性:防止指令重新排序

public class Singleton {  
    private static volatile Singleton instance;  
    
    private Singleton() {}  
    
    public static Singleton getInstance() {  
        if (instance == null) {            // 第一次检查  
            synchronized (Singleton.class) {  
                if (instance == null) {     // 第二次检查
                    // 1.分配内存空间
                    // 2.在内存中初始化对象
                    // 3.将引用指向分配的内存
                    // volatile 防止 1 2 3 被重排序成 1 3 2
                    instance = new Singleton();  
                }  
            }  
        }  
        return instance;  
    }  
}  
0

评论区