volatile关键字的详解-并发编程的体现

xl_echo编辑整理,欢迎转载,转载请声明文章来源。欢迎添加echo微信(微信号:t2421499075)交流学习。 百战不败,依不自称常胜,百败不颓,依能奋力前行。——这才是真正的堪称强大!!


参考书籍:《Java高并发编程详解》。尊重原创,支持知识付费,以下内容标记有摘抄的为该书内容,如需查看该书的对应知识点,请购买原版书籍。

参考文章列表:


volatile

什么是volatile

volatile和synchronized相比,volatile被称为轻量级锁,并且能实现部分synchronized的语义。它在处理多线程并发的时候主要保证了共享资源的可见性,该功能可以理解为一个线程修改某一个共享变量的时候,另外一个变量可以读到该共享变量的值。

资源无可见性在代码中产生的问题

基于了解程序原理就先上代码观察结果的思想,我们可以通过观察下面这段代码,先对volatile的基本体现有一个了解。

以下代码来自摘抄

public class VolatileFoo {

    final static int MAX = 5;

    static int init_value = 0;
    //static volatile int init_value = 0;

    public static void main(String[] args) {

        new Thread(() -> {
            int localValue = init_value;
            while (localValue < MAX) {
                if (init_value != localValue) {
                    System.out.printf("The init_value is update to [%d]\n", init_value);
                    localValue = init_value;
                }
            }
        }, "Reader").start();

        new Thread(() -> {
            int localValue = init_value;
            while (localValue < MAX) {
                System.out.printf("The init_value will be changed to [%d]\n", ++localValue);
                init_value = localValue;
                try {
                    TimeUnit.SECONDS.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "Updater").start();
    }

}

如果我们对volatile没有了解的情况下,我们看到以上代码会觉得这就是实现创建了两个线程不断的去交替输出一个变量的功能。观看代码确实就是localValue不断的变更,直到localValue等于5的时候,while循环才结束。但是我们可以看到以下结果,与这里的结论不符,证明我们的结论有误
图片

实际的输出结果是我们的Updater线程一直在输出,当localValue=5的时候,我们的Updater线程就会结束循环。而我们的Reader线程却一直在执行,并是处于死循环的状态。这里我们可以看到Reader线程读到的localValue的值应该是一直没有改变,也能够明显的看到一个问题,那就是两个线程访问一个变量,Updater修改变量的值后Reader线程并没有获取到这个值的变化。

volatile的初体验

出现以上问题之后,我们可以看到共享变量的可见性它的重要性,解决上面程序的问题其实也比较简单,只需要在上面做一个小修改即可。将init_value使用volatile进行修饰,其他的不变,我们再一次观察输出结果。
图片

通过输出结果我们可以看到,当Updater线程对init_value进行了改变之后,我们的Reader线程有效的观察到了这个变量的变化,并且跟着输出了Reader线程观察到init_value的结果。和上面不一样的在于,这段代码都能有效的结束程序

深入了解volatile,并有效掌握实现的方式还需要去了解->CPU硬件的运行和JMM内存模型。CPU与我们程序运行之间的关系是怎么样的,底层是怎么出现这些错误的使我们了解volatile必不可少需要掌握的要点。

我们编写的程序与CPU执行的关系

在我们的程序运行中,我们一个程序被CPU有效执行的一个过程要经过写入硬盘,内存加载,CPU访问执行。,硬盘、内存和CPU之间又有很大的区别。

  • 硬盘:存储资料和软件等数据的设备,有容量大,断电数据不丢失的特点。也被人们称之为“数据仓库”。我们编写的程序就是存储在硬盘里面
  • 内存:1. 负责硬盘等硬件上的数据与CPU之间数据交换处理;2. 缓存系统中的临时数据。3. 断电后数据丢失。可以称为他就是硬盘和CPU之间的桥梁,并且我们的程序编写完成存储到硬盘中之后,开始执行就会被加载进入内存,并等待CPU对内存进行寻址操作。
  • CPU:中央处理单元(Cntral Pocessing Uit)的缩写,也叫处理器,是计算机的运算核心和控制核心。执行我们编写的代码CPU只是接收到执行指令,然后对内存进行寻址操作。

硬盘、内存和CPU的存取速度是递增的,内存比硬盘要快很多,但是CPU又比内存块很多倍,CPU的存取速度快到内存都跟不上,所以在CPU和内存之间出现了一个新的东西,那就是CPU Cache。cache的容量远远小于主存,因此出现cache miss在所难免,这也是我们为什么会出现数据问题的关键所在。

CPU cache结构及cache操作数据导致数据不一致的问题

CPU中cache的结构我们可以打开任务管理器,点击性能即可以看到。多核CPU的结构与单核相似,但是多了所有CPU共享的L3三级缓存。在多核CPU的结构中,L1和L2是CPU私有的,L3则是所有CPU核心共享的。
图片

在我们的系统中,由于短板效应,导致即时我们CPU速度再快也没有办法发挥它的能力。由于内存的读取速度远低于CPU,所以导致我们的程序执行速度,被内存限制。但是当cache出现之后完全改变了这个情况,它极大的增大了CPU的吞吐量。CPU只需要到cache中进行读取和写入操作即可,cache会在之后将结果同步到内存。但是当多线程情况下就会出现问题,每个线程都有自己的工作内存,本地内存,对应CPU中Cache。当多个线程同时操作一个变量的时候,都会进行读取到CPU Cache当中,然后在同步会主内存,这样就会导致数据结果不一致。也就是我们平时看到的,多线程结果与预期不一致的问题。

Java内存模型

Java的内存模型(Java Memory Mode)制定了Java虚拟机如何与计算机的主存进行工作,理解Java内存模型对于编写并发程序非常重要。在CPU cache当中我们使用文字描述了多线程情况下出现结果不一致情况,这里我们可以通过Java内存模型的图解来更直观的看到这个情况是怎么出现的。
图片

图中线程1的工作内存和线程2的工作内存就是我们上面描述的当有多个线程操作一个变量时,每个线程就会将变量复制一份到自己的工作内存当中。当我们的多线程执行的时候,每一个线程赋值一份变量,都对值进行修改,当共享变量不可见的时候,最终就会导致结果不一致。

并发编程的三个重要特性

  • 原子性
    • 原型性是指一个操作的完整性,要么该操作改变的值或者资源全部成功,要么全部不成功。
  • 有序性
    • 所谓有序性就是指代码在执行过程当中的先后顺序。
  • 可见性
    • 可见性在我们最上面的例子里面就展现了,就是一个线程修改共享变量的值的时候,另外一个线程能够看到这个变量的值被改变。

在我们多线程并发编程当中,它的三大特性是保证并发执行不出现错误的关键,volatile我们目前能够看到在并发编程当中能够保证可见性。除了可见性外还其实它还可以保证有序性,只是不能保证原子性而已。假若能够保证原子性,它和synchronize的作用基本那就是一样的,只是底层的实现原理不一样而已。

volatile如何保证有序性(摘抄)

volatile关键字对顺序性的保证就比较霸道,直接禁止JVM和处理器对volatile关键字修饰的指令重新排序,但是对于volatile前后无依赖关系的指令则可以随便怎么排序。

volatile可见性的底层实现原理

volatile底层的实现其实是通过lock关键字进行实现的,我们可以去获取class的汇编码,当使用volatile修饰和不使用volatile的代码分别获取到class的汇编码,然后进行对比,你会发现标有volatile的变量在进行写操作时,会在前面加上lock质量前缀。而lock指令前缀会做如下两件事

  • 将当前处理器缓存行的数据写回到内存。lock指令前缀在执行指令的期间,会产生一个lock信号,lock信号会保证在该信号期间会独占任何共享内存。lock信号一般不锁总线,而是锁缓存。因为锁总线的开销会很大。
  • 将缓存行的数据写回到内存的操作会使得其他CPU缓存了该地址的数据无效。

volatile和synchronize的区别

  • volatile只能修饰实例变量或者类变量,synchronize只能修饰方法或者语句块
  • volatile无法保证原子性,synchronize能够保证原子性
  • volatile和synchronize都能保证有序性,只是实现方式不一样
  • volatile不会使线程陷入阻塞,synchronize相反

总结

volatile被称为轻量级的synchronize是因为他能够有效的实现并发编程的有序性和可见性。但是同时它有自己的缺点,比如不能保证原子性的问题。部分场景能够直接volatile,比如对线程的唤起和关闭。synchronize虽然能够保证并发编程有点三要素,但是会造成线程阻塞。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
### 回答1: volatile是Java中的一个关键字,用于修饰变量。它的作用是告诉编译器,该变量可能会被多个线程同时访问,因此需要特殊处理,以保证线程安全。 具体来说,volatile关键字有以下几个特点: 1. 可见性:当一个线程修改了volatile变量的值,其他线程能够立即看到这个修改。 2. 有序性:volatile变量的读写操作会按照程序的顺序执行,不会被重排序。 3. 不保证原子性:虽然volatile变量能够保证可见性和有序性,但是它并不能保证多个线程同时修改变量时的原子性。 因此,如果需要保证原子性,需要使用synchronized关键字或者Lock接口来进行同步。 总之,volatile关键字是Java中用于保证多线程访问变量的安全性的一种机制,它能够保证可见性和有序性,但是不能保证原子性。 ### 回答2: Java中的volatile关键字是一种轻量级的同步机制,用于确保多个线程之间的可见性和有序性。它可以用于修饰变量、类和方法。 1. 修饰变量:当一个变量被volatile修饰时,它会被立即写入到主内存中,并且每次读取变量时都会从主内存中重新获取最新的值。这样可以保证多个线程操作同一个变量时的可见性和一致性。 2. 修饰类:当一个类被volatile修饰时,它的实例变量就会被同步,而且每个线程都会获取最新的变量值。这样可以保证多线程操作同一对象时的可见性和一致性。 3. 修饰方法:当一个方法被volatile修饰时,它的调用会插入内存栅栏(memory barrier)指令,这可以保证方法调用前的修改操作都已经被写入主内存中,而方法调用后的读取操作也会重新从主内存中读取最新值。这样可以确保多线程之间的调用顺序和结果可见性。 需要注意的是,volatile并不能完全取代synchronized关键字,它只适用于并发度不高的场景,适用于只写入不读取的场景,不能保证复合操作的原子性。 总之,volatile关键字在Java中具有广泛的应用,可以保证多线程之间的数据同步和可见性,但也需要谨慎使用,以免造成数据不一致和性能问题。 ### 回答3: Java中的volatile关键字意味着该变量在多个线程之间共享,并且每次访问该变量时都是最新的值。简单来说,volatile保证了线程之间的可见性和有序性。下面我们详细解释一下volatile的用法和作用。 1. 线程之间的可见性 volatile关键字保证了对该变量的读写操作对所有线程都是可见的。在没有用volatile关键字修饰变量的情况下,如果多个线程并发访问该变量,每个线程都会从自己的线程缓存中读取该变量的值,而不是直接从主存中读取。如果一个线程修改了该变量的值,但是其他线程不知道,那么可能导致其他线程获取到的数据不是最新的,从而引发一系列问题。而用了volatile关键字修饰该变量后,每次修改操作都会立即刷新到主存中,其他线程的缓存中的变量值也会被更新,从而保证了线程之间的可见性。 2. 线程之间的有序性 volatile关键字也保证了线程之间的有序性。多个线程并发访问同一个volatile变量时,JVM会保证每个线程按照程序指定的顺序执行操作。例如,在一个变量被volatile修饰的情况下,多个线程同时对该变量进行读写操作,JVM会保证先执行写操作的线程能够在后续的读操作中获取到最新的变量值。这么做的好处是,可以避免出现线程间操作顺序的乱序问题,从而保证了程序的正确性。 需要注意的是,并不是所有的变量都需要用volatile关键字修饰。只有在多个线程之间共享变量并且对变量的读写操作之间存在依赖关系的情况下,才需要使用volatile关键字。此外,volatile关键字不能保证原子性,如果需要保证操作的原子性,需要使用synchronized或者Lock等其他并发工具。 总之,volatile关键字是Java中非常重要的关键字之一,它可以在多个线程之间保证可见性和有序性,从而保证了程序的正确性。在开发过程中,我们应该根据具体情况来选择是否使用volatile关键字,以及如何使用它。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

xlecho

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值