Memory Model -- 13 -- volatile的基本概念

本文详细解析了Java中的volatile关键字,探讨其保证共享变量可见性和禁止指令重排序的特性,通过实例说明volatile在多线程环境中的应用及限制。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

原文链接:Memory Model – 13 – volatile的基本概念


相关文章:


volatile 是 Java 虚拟机提供的最轻量级的同步机制,其具有以下两项特性

  • 保证被 volatile 修饰的共享变量对所有线程都是可见的,所谓可见性,是指当一个线程修改了被 volatile 修饰的共享变量时,新值对于其他线程来说是可以立即得知的

  • 禁止指令重排序优化


一、volatile 的可见性

  • volatile 变量在各个线程的工作内存中不存在一致性的问题,但是在 Java 中运算操作符并非是原子操作,因此 volatile 变量的运算在并发情况下是线程不安全的

  • 线程不安全示例

    public class VolatileTest {
        
        public static volatile int count = 0;
        
        public static void increase() {
            count++;
        }
    }
    
    • 如上所示,若当前存在线程 A 和线程 B 两个线程,同时调用了 increase() 方法,由于 count++ 操作是线程不安全的 (先读取值,再进行运算,最后写入新值),因此在线程 A 读取 count 旧值和写入 count 新值的期间,线程 B 可能已经调用 increase() 方法结束 (即 count 的值已经被加了 1)

    • 此时由于 count 被 volatile 关键字修饰,因此线程 A 可以得知 count 已经被修改为 1,但此时线程 A 已经读取了 count 的旧值 (不会重复读取),因此线程 A 调用 increase() 方法得到的值仍然会 1,所以会存在线程不安全的问题

  • 由于 volatile 变量只能保证可见性,在不符合以下两条规则的运算场景中,我们仍然需要通过加锁来保证原子性

    • 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值

    • 变量不需要与其他的状态变量共同参与不变约束

  • 线程安全示例

    public class VolatileTest {
        
        volatile boolean shutdown;
        
        public void shutdown() {
            shutdown = true;
        }
        
        public void doWork() {
            while (!shutdown) {
                // ...
            }
        }
    }
    
    • 如上所示,我们通过 volatile 变量 shutdown 来控制并发,当调用 shutdown() 方法时,可以保证所有线程中执行 doWork() 方法都立即停下来

二、volatile 禁止指令重排序

  • 在了解 volatile 禁止指令重排序前,我们要先了解一个概念 - 内存屏障 (Memory Barrier)

    • 保证特定操作的执行顺序

    • 保证某些变量的内存可见性

  • 通过插入内存屏障指令,来禁止对在内存屏障前后的指令进行重排序优化,即重排序时不能把内存屏障后面的指令重排序到内存屏障之前的位置

  • 强制刷出各种 CPU 的缓存数据,因此任何 CPU 上的线程都能读取到这些数据的最新版本

  • 单例模式示例 (懒汉式)

    • 众所周知,懒汉式在多线程环境下是线程不安全的,代码如下

      public class Singleton {
          
          private static Singleton instance;
          
          private Singleton() {}
          
          public static Singleton getInstance() {
              if (instance == null) {
                  synchronized(Singleton.class) {
                      if (instance == null) {
                          instance = new Singleton();
                      }
                  }
              }
              return instance;
          }
      }
      
      • 如上所示,使用了双重检测来实现饿汉式,但在多线程环境下仍然是线程不安全的,原因就在于 instance = new Singleton(); 操作会存在指令重排序,从而导致线程不安全

      • instance = new Singleton(); 操作分为以下三步

        • 1、分配对象内存空间

        • 2、初始化对象

        • 3、设置 instance 指向刚分配的内存地址 (此时 instance != null)

      • 而此时可能会发生指令重排序,由原先的 1、2、3 顺序变为 1、3、2 顺序,因为步骤 2 与步骤 3 不存在数据依赖的关系,且执行程序的结果在单线程中并没有改变

        • 1、分配对象内存空间

        • 3、设置 instance 指向刚分配的内存地址 (此时 instance != null)

        • 2、初始化对象

      • 所以当一个线程判断一个 instance 不为 null 时,instance 实例可能并未完成初始化,从而导致线程安全问题,因此我们可以通过 volatile 来禁止指令重排序

      • 解决方法

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

三、归纳总结

  • volatile 的作用

    • 保证共享变量的内存可见性

    • 禁止指令重排序优化

  • volatile 变量为何立即可见?

    • 当写入一个 volatile 变量时,Java 内存模型会把该线程对应工作内存中的共享变量值刷新到主内存中

    • 当读取一个 volatile 变量时,Java 内存模型会把该线程对应工作内存中的共享变量值设为无效,然后从主内中重新获取该共享变量值

  • 什么是内存屏障?

    • 内存屏障,也称内存栅栏、内存栅障、屏障指令,是一类同步屏障指令,它使得 CPU 或编译器在对内存进行操作时,严格按照一定的顺序来执行;也就是说,在内存屏障之前的指令和内存屏障之后的指令不会因为系统优化等原因而导致乱序
  • 内存屏障的作用

    • 保证特定操作的执行顺序

    • 保证共享变量的内存可见性

  • volatile 和 synchronized 的区别

    • volatile 只能修饰成员变量;synchronized 可以修饰方法、代码块

    • volatile 可以保证共享变量修改的可见性,但不能保证原子性;synchronized 可以保证共享变量修改的可见性和原子性

    • 在多线程情况下,volatile 不会造成线程阻塞;synchronized 则可能会造成线程阻塞

    • volatile 用于解决变量在多个线程之间的可见性;synchronized 则解决的是多个线程之间访问资源的同步性

### PyCharm 打开文件显示全的解决方案 当遇到PyCharm打开文件显示全的情况时,可以尝试以下几种方法来解决问题。 #### 方法一:清理缓存并重启IDE 有时IDE内部缓存可能导致文件加载异常。通过清除缓存再启动程序能够有效改善此状况。具体操作路径为`File -> Invalidate Caches / Restart...`,之后按照提示完成相应动作即可[^1]。 #### 方法二:调整编辑器字体设置 如果是因为字体原因造成的内容显示问题,则可以通过修改编辑区内的文字样式来进行修复。进入`Settings/Preferences | Editor | Font`选项卡内更改合适的字号大小以及启用抗锯齿功能等参数配置[^2]。 #### 方法三:检查项目结构配置 对于某些特定场景下的源码视图缺失现象,可能是由于当前工作空间未能正确识别全部模块所引起。此时应该核查Project Structure的Content Roots设定项是否涵盖了整个工程根目录;必要时可手动添加遗漏部分,并保存变更生效[^3]。 ```python # 示例代码用于展示如何获取当前项目的根路径,在实际应用中可根据需求调用该函数辅助排查问题 import os def get_project_root(): current_file = os.path.abspath(__file__) project_dir = os.path.dirname(current_file) while not os.path.exists(os.path.join(project_dir, '.idea')): parent_dir = os.path.dirname(project_dir) if parent_dir == project_dir: break project_dir = parent_dir return project_dir print(f"Current Project Root Directory is {get_project_root()}") ```
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值