首页 > 编程语言 >Java的volatile

Java的volatile

时间:2023-05-31 13:23:16浏览次数:48  
标签:Java 变量 指令 volatile 内存 排序 处理器

介绍 volatile

volatile 关键字可以说是 Java 虚拟机提供的最轻量级的同步机制,但是它并不容易被正确、完整地理解,以至于许多程序员都习惯去避免使用它,遇到需要处理多线程数据竞争问题的时候一律使用 synchronized 来进行同步。了解 volatile 变量的语义对理解多线程操作的其他特性很有意义。

在众多保障并发安全的工具中选用 volatile 的意义:它能让我们的代码比使用其他的同步工具更快吗?在某些情况下, volatile 的同步机制的性能确实要优于锁(使用 synchronized 关键字或 java.util.concurrent 包里面的锁),但是由于虚拟机对锁实行的许多消除和优化,使得我们很难确切地说 volatile 就会比 synchronized 快上多少。如果让 volatile 自己与自己比较,那可以确定一个原则:volatile 变量读操作的性能消耗与普通变量读操作的性能消耗几乎没有什么差别,但是volatile 变量的写操作则可能会比普通变量的写操作慢上一些,因为 volatile 变量的写操作需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。不过即便如此,大多数场景下 volatile 的总开销仍然要比锁来得更低。我们在 volatile 与锁中选择的唯一判断依据仅仅是 volatile 的语义能否满足使用场景的需求。

volatile 的语义

Java 内存模型为 volatile 专门定义了一些特殊的访问规则。当一个变量被 volatile 修饰时,这个变量将具备两项特性:

  • 第一项特性是:保证此变量对所有线程的可见性,而普通变量则不能保证这一点。这里的“可见性”指的是:一个线程修改了共享变量的值,其他的线程能够立即得知这个修改。Java 内存模型通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性。当一个变量被 volatile 修饰,线程在读取和写入这个变量的值时,在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值。
  • 第二项特性是:volatile 关键字禁止指令重排序。“volatile 变量的赋值操作” 之前的指令无法重排序到 “赋值操作” 之后的位置,“volatile 变量的赋值操作” 之后的指令无法重排序到 “赋值操作” 之前的位置。volatile 变量的赋值操作在本地代码中插入 lock 前缀的指令(内存屏障指令,重排序时不能把内存屏障后面的指令重排序到内存屏障之前的位置),lock 前缀的指令的作用是将本处理器的缓存写入内存,lock 前缀的指令把修改同步到内存时,意味着所有之前的操作都已经执行完成,这样便形成了 “指令重排序无法越过内存屏障” 的效果。普通的变量仅会保证在方法的执行过程中,所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值操作的顺序与程序代码中的执行顺序一致。

volatile 变量只能保证可见性,因此在不符合以下两条规则的运算场景中,我们仍然要通过加锁(使用 synchronized、java.util.concurrent 中的锁或原子类)来保证原子性:

  • 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。
  • 变量不需要与其他的状态变量共同参与不变约束。

指令重排序

指令重排序是指处理器为了提高指令的执行效率,对指令序列进行重新排序的一种优化技术。但并不是说指令任意重排,处理器必须能正确处理指令依赖情况,保障程序能得出正确的执行结果。譬如指令 1 把地址 A 中的值加 10,指令 2 把地址 A 中的值乘以 2,指令 3 把地址 B 中的值减去 3,这时指令 1 和指令 2 是有依赖的,它们之间的顺序不能重排,(A+10) * 2 与 A * 2+10 显然不相等,但指令 3 可以重排到指令 1、2 之前或者中间,只要保证处理器执行后面依赖到 A、 B 值的操作时能获取正确的 A 和 B 值即可。所以在同一个处理器中,重排序过的代码看起来依然是有序的。

例子

通过一个例子来看看为何指令重排序会干扰程序的并发执行。演示程序如代码清单12-4所示。代码清单12-4中所示的程序是一段伪代码,其中描述的场景是开发中常见的配置读取过程,只是我们在处理配置文件时一般不会出现并发,所以没有察觉这会有问题。读者试想一下,如果定义 initialized 变量时没有使用 volatile 修饰,就可能会由于指令重排序的优化,导致位于线程 A 中最后一条代码 “initialized=true” 被提前执行(这里虽然使用 Java 作为伪代码,但所指的重排序优化是机器级的优化操作,提前执行是指这条语句对应的汇编代码被提前执行),这样在线程 B 中使用配置信息的代码就可能出现错误,而 volatile 关键字则可以避免此类情况的发生。

// 代码清单12-4 指令重排序

Map configOptions;
char[] configText;
// 此变量必须定义为volatile
volatile boolean initialized = false;

// 假设以下代码在线程 A 中执行
// 模拟读取配置信息, 当读取完成后
// 将initialized设置为true,通知其他线程配置可用
configOptions = new HashMap();
configText = readConfigFile(fileName);
processConfigOptions(configText, configOptions);
initialized = true;

// 假设以下代码在线程 B 中执行
// 等待initialized为true,代表线程A已经把配置信息初始化完成
while (!initialized) {
    sleep();
}
// 使用线程A中初始化好的配置信息
doSomethingWithConfig();

内存屏障

再举一个可以实际操作运行的例子来分析 volatile 关键字是如何禁止指令重排序的。代码清单12-5所示是一段标准的双重检测(Double Check Lock,DCL)单例代码,可以观察加入 volatile 和未加入 volatile 关键字时所生成的汇编代码的差别。

// 代码清单12-5 DCL单例模式
public class Singleton {
    private volatile static Singleton instance;

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

    public static void main(String[] args) {
        Singleton.getInstance();
    }
}

编译后,这段代码对 instance 变量赋值的部分如代码清单12-6所示。

image-20230515174320144.png

通过对比发现,关键变化在于有 volatile 修饰的变量,赋值后(前面 mov %eax,0x150(%esi) 这句便是赋值操作)多执行了一个 “lock addl $0x0,(%esp)” 操作,这个操作的作用相当于一个内存屏障(Memory Barrier 或 Memory Fence,指重排序时不能把内存屏障后面的指令重排序到内存屏障之前的位置)。

  • 只有一个处理器访问内存时,并不需要内存屏障。因为在同一个处理器中,处理器能正确处理指令依赖情况,保障程序能得出正确的执行结果,重排序过的代码看起来依然是有序的;
  • 但如果有两个或更多处理器访问同一块内存,且其中有一个在观测另一个,就需要内存屏障来保证一致性了。

这句指令中的 “addl $0x0,(%esp)”(把 ESP 寄存器的值加 0)显然是一个空操作,之所以用这个空操作而不是空操作专用指令 nop,是因为 IA32 手册规定 lock 前缀不允许配合 nop 指令使用。这里的关键在于 lock 前缀,查询 IA32 手册可知,lock 前缀的作用是将本处理器的缓存写入了内存,该写入动作也会引起别的处理器或者别的内核无效化(Invalidate) 其缓存,这种操作相当于对缓存中的变量做了一次 “store 和 write” 操作。所以通过这样一个空操作,可让前面 volatile 变量的修改对其他处理器立即可见。


那为何说 volatile 禁止指令重排序呢?从硬件架构上讲,指令重排序是指处理器采用了允许将多条指令不按程序规定的顺序分开发送给各个相应的电路单元进行处理。但并不是说指令任意重排,处理器必须能正确处理指令依赖情况,保障程序能得出正确的执行结果。譬如指令 1 把地址 A 中的值加 10,指令 2 把地址 A 中的值乘以 2,指令 3 把地址 B 中的值减去 3,这时指令 1 和指令 2 是有依赖的,它们之间的顺序不能重排,(A+10) * 2 与 A * 2+10 显然不相等,但指令 3 可以重排到指令 1、2 之前或者中间,只要保证处理器执行后面依赖到 A、 B 值的操作时能获取正确的 A 和 B 值即可。所以在同一个处理器中,重排序过的代码看起来依然是有序的。因此,lock addl $0x0,(%esp) 指令把修改同步到内存时,意味着所有之前的操作都已经执行完成,这样便形成了 “指令重排序无法越过内存屏障” 的效果。

Doug Lea 列出了各种处理器架构下的内存屏障指令:http://gee.cs.oswego.edu/dl/jmm/cookbook.html。

参考资料

《深入理解Java虚拟机》第 12 章 Java 内存模型与线程 12.3.3 对于 volatile 型变量的特殊规则

标签:Java,变量,指令,volatile,内存,排序,处理器
From: https://www.cnblogs.com/feiyu2/p/volatile.html

相关文章

  • 序列化Java对象重命名字段,@JSONField、@JsonProperty、@SerializedName
    @JSONField主要用于返回出参转换这个注解分别可以注解在实体类的属性、setter和getter方法上publicclassTest{/*注解在属性上的时候可以设置一些序列化、格式化的属性@JSONField(serialize=false)---->序列化的时候忽略这个属性@JSO......
  • java9&10&11
    java9语法改进:接口的私有方法Java8中规定接口中的方法除了抽象方法之外,还可以定义静态方法和默认方法。一定程度上,扩展了接口的功能,此时的接口更像是一个抽象类。在Java9中,接口更加的灵活和强大,连方法的权限修饰符都可以声明为private的了,此时方法将不会称为你对外暴露API的......
  • 用Java开发HTTP代理服务器
    HTTP代理服务器是一种网络应用,它充当位于客户端和目标服务器之间的中间节点,将客户端发出的HTTP请求转发给目标服务器,并将目标服务器返回的HTTP响应内容回传给客户端。通过使用代理服务器,客户端可以避免直接访问目标服务器,从而更加安全地访问互联网资源。以下是使用Java开发HTTP......
  • java springboot 项目中静态资源无法访问的问题
    默认resource/static目录下的静态html文件无法访问,需要配置。您可以在SpringBoot的配置类中添加以下内容来手动配置静态资源目录:@ConfigurationpublicclassWebMvcConfigimplementsWebMvcConfigurer{@OverridepublicvoidaddResourceHandlers(ResourceHan......
  • java简单实现布隆过滤器
    为了方便理解布隆过滤器,java简单实现了下,特点:仅用做一次运算就可以判断存在不存在,但是只能精确的判断值不存在,不能精确的判断值存在publicclassBlTest{privatefinalintf=1024;//负载因子,值越大判断的越精准,但是所占的空间也越大int[]bArray;BlTest(){......
  • Java大文件分片上传/多线程上传解决方案
    ​ javaweb上传文件上传文件的jsp中的部分上传文件同样可以使用form表单向后端发请求,也可以使用ajax向后端发请求    1.通过form表单向后端发送请求         <formid="postForm"action="${pageContext.request.contextPath}/UploadServlet"method="post"e......
  • java面试题
    一、面向对象的特性有哪些?封装(Encapsulation):将数据和方法封装在一个类中,通过访问修饰符控制数据的访问权限,提高程序的安全性和可维护性。继承(Inheritance):可以从父类继承属性和方法,避免重复编写代码,简化程序设计和维护。多态(Polymorphism):同一种类型的对象,可以以不同的方式......
  • java轻型内存队列处理demo
    java轻型内存队列处理demo@ComponentpublicclassConcurrentLinkedQueueUtils{staticAtpLogBizatpLogBiz;staticAuditLogtTmpDataServiceauditLogDataService;staticConcurrentLinkedQueueconList=newConcurrentLinkedQueue();privatestaticvo......
  • java中属性和局部变量的作用域
    1.类中的属性和局部变量可以重名,访问时遵循就近原则2.在同一个作用域中({}大括号括起来的),如在一个成员方法中,两个局部变量不能重名3.属性的生命周期长,随着对象的创建而创建,随着对象的销毁而销毁;局部变量的生命周期短,随着它所属的代码块执行而创建,随着代码快的结束而销毁。......
  • java同步mysql的数据到PostgreSQL时报错ERROR: invalid byte sequence for encoding "
    最近,同事在做一个功能,通过java程序将mysql中的一张表的数据同步到pgsql中,在同步过程中,插入到pgsql中出现了如下错误:`###Errorupdatingdatabase.Cause:org.postgresql.util.PSQLException:ERROR:invalidbytesequenceforencoding"UTF8":0x00在位置:unnamedportalpa......