Java的volatile关键字使用分析

volatile 是 Java 提供的一个轻量级同步机制,用于保证变量在多个线程之间的可见性。它可以修饰变量(但不能修饰方法或代码块),告诉 Java 虚拟机对该变量的操作要遵循特定的规则。

主要功能

  1. 内存可见性
    • 当一个线程修改了被 volatile 修饰的变量,其值会立即更新到主内存中,其他线程能够立刻看到最新值。
  2. 禁止指令重排序
    • 使用 volatile 修饰的变量,会在读和写操作时加入内存屏障,防止指令重排序。这可以确保变量的赋值操作不会被编译器或 CPU 优化到其他位置。

内存可见性

Java 内存模型(Java Memory Model,简称 JMM)旨在屏蔽不同硬件和操作系统之间的内存访问差异。由于不同的硬件架构和操作系统在内存访问机制上存在差异,这可能导致相同的 Java 代码在不同平台上表现出不同的行为。JMM 的作用是确保这些差异不会影响程序的正确性,使得相同的代码在不同平台上的行为保持一致。

根据 JMM 的规定:

  • 所有实例变量和静态变量都存储在主内存中。
  • 每个线程都有自己的工作内存,其中保存了该线程使用的变量的副本。
  • 线程对变量的所有读写操作都在其工作内存中进行,而不是直接操作主内存中的变量。

这种设计确保了多线程环境下的内存可见性和数据一致性。

然而,Java 内存模型会带来一个新的问题,那就是内存可见性问题,也就是当某个线程修改了主内存中共享变量的值之后,其他线程不能感知到此值被修改了,它会一直使用自己工作内存中的“旧值”,这样程序的执行结果就不符合我们的预期了,这就是内存可见性问题,我们用以下代码来演示一下这个问题:

package com.guide.fullstack;

public class CompassExample {
    // 定义一个表示指南针状态的标志
    private static boolean pointingNorth = false;

    public static void main(String[] args) {
        // 模拟指南针的方向监测线程
        Thread compassMonitor = new Thread(() -> {
            System.out.println("指南针监测线程启动...");
            while (!pointingNorth) {
                // 一直等待指南针指向北方
            }
            System.out.println("指南针已经指向北方!");
        });
    
        // 模拟设置指南针方向的线程
        Thread compassSetter = new Thread(() -> {
            try {
                Thread.sleep(2000); // 模拟设置方向需要一定时间
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("设置指南针指向北方...");
            pointingNorth = true; // 修改状态
        });
    
        // 启动两个线程
        compassMonitor.start();
        compassSetter.start();
    }

}

运行这个程序会发现输出永远停留在 指南针监测线程启动...,因为第一个线程永远感知不到第二个线程对于变量值的修改

禁止指令重排序

指令重排序是指编译器或 CPU 为了优化程序的执行性能,而对指令进行重新排序的一种手段。

指令重排序的实现初衷是好的,但是在多线程执行中,如果执行了指令重排序可能会导致程序执行出错。指令重排序最典型的一个问题就发生在单例模式中,比如以下问题代码:

public class CommonUtil {
    // 单例模式
    private static CommonUtil instance;

    private CommonUtil() {
    }

    public static CommonUtil getInstance() {
        if (instance == null) {
            synchronized (CommonUtil.class) {
                if (instance == null) {
                    instance = new CommonUtil();
                }
            }
        }
        return instance;
    }
}

以上问题发生在代码这一行instance = new CommonUtil();,这行代码看似只是一个创建对象的过程,然而它的实际执行却分为以下 3 步:

  1. 创建内存空间。
  2. 在内存空间中初始化对象 CommonUtil。
  3. 将内存地址赋值给 instance 对象(执行了此步骤,instance 就不等于 null 了)。

如果此变量不加 volatile,那么线程 1 在执行到上述代码时就可能会执行指令重排序,将原本是 1、2、3 的执行顺序,重排为 1、3、2。但是特殊情况下,线程 1 在执行完第 3 步之后,如果来了线程 2 执行到上述代码,判断 instance 对象已经不为 null,但此时线程 1 还未将对象实例化完,那么线程 2 将会得到一个被实例化“一半”的对象,从而导致程序执行出错

为什么日常编程中使用得不多

无法保证原子性

volatile只保证可见性,不能保证操作的原子性。例如:

private volatile int count = 0;

public void increment() {
    count++; // 非原子操作,可能产生竞态条件
}

这种情况需要使用 synchronizedAtomicInteger 来替代。

性能问题

  • 虽然 volatile 比锁轻量级,但仍会引入内存屏障,影响性能。因此,除非确实需要,通常避免使用。

使用场景受限

  • 只有在变量状态的变化与其他变量无关,并且不涉及复杂的并发逻辑时,volatile 才适用。更多情况下,锁或线程安全工具类(如 java.util.concurrent 提供的工具)更为灵活和可靠。

总结

volatile 是一个简单但重要的关键字,它为并发编程提供了基本的内存可见性保证。然而,由于其功能局限性,在日常开发中常常被更强大的工具(如 synchronizedLock 或并发包)取代。volatile 更适合在需要低延迟且轻量同步的场景中使用,或者作为标志位和简单状态管理的工具。

欢迎关注公众号:“全栈开发指南针”
这里是技术潮流的风向标,也是你代码旅程的导航仪!🚀
Let’s code and have fun! 🎉

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值