深入理解volatile关键字

7 篇文章 0 订阅
3 篇文章 0 订阅

深入理解volatile关键字

CPU和JMM

初识volatile关键字

  • 以一个demo来直观的感受下volatile关键字的作用。先将下面代码的中的volatile去掉运行,然后再加上运行main方法,观察两次运行的区别。
public class VolatileFoo {
    // init_value的最大值
    private final static int MAX = 5;
    // init_value的初始值、
    private static volatile int initValue = 0;

    public static void main(String[] args) {
        // 启动一个Reader线程,当发现local_value和init_value不同时,则输出init_value被修改的信息
        new Thread(() -> {
            int localValue = initValue;
            while (localValue < MAX) {
                if (initValue != localValue) {
                    System.out.printf("The initValue is updated to [%d]\n", initValue);
                    // 对localValue进行重新赋值
                    localValue = initValue;
                }
            }
        }, "Reader").start();

        // 启动一个Updater线程,主要用于对initValue的修改,对localValue>=5的时候则退出生命周期
        new Thread(() -> {
            int localValue = initValue;
            while (localValue < MAX) {
                // 修改initValue
                System.out.printf("The initValue will be changed to [%d]\n", ++localValue);
                initValue = localValue;
                try {
                    // 暂时休眠,目的是为了使Reader线程能够来得及输出变化内容
                    TimeUnit.MILLISECONDS.sleep(20);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }, "Updater").start();
    }
}
  • 可以观察到,没有volitale关键字,运行的情况如下。似乎Reader线程从来没有执行过一般。其实,仔细观察你会发现程序并没有终止,其实是进入了死循环状态了。
The initValue will be changed to [1]
The initValue will be changed to [2]
The initValue will be changed to [3]
The initValue will be changed to [4]
The initValue will be changed to [5]
  • 加上volatile关键字后,运行结果如下,这才是我们预期想要的结果。
The initValue will be changed to [1]
The initValue is updated to [1]
The initValue will be changed to [2]
The initValue is updated to [2]
The initValue will be changed to [3]
The initValue is updated to [3]
The initValue will be changed to [4]
The initValue is updated to [4]
The initValue will be changed to [5]
The initValue is updated to [5]

在说明为何会出现这种情况之前,首先得介绍下CPU结构和Java的内存模型。

CPU

CPU Cache模型

  • 首先,我们要清楚一件事。那就是**CPU所能访问的所有数据只能是计算机的主存(RAM),并且CPU处理数据的速度远远大于内存的访问速度**。对于共享数据来说,这就会产生数据一致性问题。所有就有了CPU Cache模型。
  • 现在的CPU Cache的数量可以达到3级了,点击计算机的任务管理器 -> 性能 -> CPU是可以观察到这个三个缓存的,分别为L1,L2,L3缓存。这其中L1CPU最近,从中读取数据的速度也是最快的。
  • CPU通过直接访问Cache的方式替代直接访问内存的方式极大地提高了CPU的吞吐能力。但是,这样将数据缓存起来,虽然提高了吞吐能力,但是带来的副作用就是数据一致性问题

缓存一致性问题

  • 解决缓存数据一致性问题,通常采取的方法无非就是如下两种:

    1. 通过总线加锁的方式;
    2. 通过缓存一致性协议。
  • 第一种方法大致可以理解为Java中加同步锁的意思。这样就会阻塞其他CPU对其他组件的访问,从而使得只有一个CPU能够访问这个变量的内存。这大大地降低的CPU的利用率,毕竟现在的计算机都是多核的了。

  • 第二种方法简单的理解就是:各个缓存间共享变量的副本是一致的。如果哪一个缓存对变量进行了修改,修改完成后会立即将新的变量值刷新到主存中。并且会导致其它缓存中的变量副本失效,只能再次从主存中获取。显然,这种方式优于第一种方法。

JMM

  • Java的内存模型(Java Memory Mode, JMM)指定了Java虚拟机如何与计算机的主存RAM进行工作。它决定了一个线程对共享变量的写入何时对其他线程可见,定义了主内存和线程之间的抽象关系,如下:
    1. 共享变量存储于主内存中,每个线程都可以访问;
    2. 每个线程都有私有的工作内存或者称之为本地内存;
    3. 工作内存只存储该线程对共享内存的副本;
    4. 线程不能直接操作主内存,只有先操作了工作内存之后才能写入主内存中;
    5. 工作内存和Java内存模型都是抽象的概念,它其实并不存在,它涵盖了缓存、寄存器、编译器优化以及硬件等。

深入volatile关键字

  • volatile关键字,就不得不说并发编程;因为volatile关键键主要作用就是数据可见性,解决数据一致性问题。
  • 并发编程的三大特性:原子性、可见性和有序性

原子性

  • 简单的说就是一些操作,要么全部成功,要么全部失败。
  • 就比如A给B转账500,A账户减500和B账户加500这两个操作必然是要成功都成功,要失败都失败。

可见性

  • 当一个线程对共享变量进行了修改,那么另外的线程可以立即看到修改后的新值。

有序性

  • 指程序代码在执行过程中的先后顺序。在JVM中不一定保证程序的执行顺序一定按照代码的抒写顺序,但是一定能保证程序执行的最终结果一致。
  • 注意:在单线程情况下,这并不会有什么问题。但是在并发多线程的情况下,指令的重排序就可能会造成不可预期的后果。

JMM保证三大特性

  • JVM采用内存模型的机制来屏蔽各个平台和操作系统之间内存访问的差异,以实现让Java程序在各种平台下达到一致的内存访问效果。比如不管在什么操作系统中,int类型的都是占用四个字节。
  • Java的内存模型规定了所有的变量都存在于主内存(RAM)当中的,而每个线程都有自己的工作内存和本地内存,线程对变量的所有操作都必须在自己的工作内存中进行,而不能直接操作主内存,并且每个线程都不能访问其他线程的工作内存和本地内存。
  • 首先明确一点的是:volatile不能保证原子性。但是可以保证可见性和一致性。
  • JMM可以通过synchronized关键字保证三大特性,也可以通过JUC中的显示锁Lock来保证。

深入解析volatile

  • volatile关键字有两层语义:
    1. 保证了不同线程之间对共享变量操作时的可见性;
    2. 禁止对指令进行重排序操作。但需要注意的是,只限于对volatile关键字修饰的变量。对于volatile前后无依赖关系的指令则可以随便这么排序。
  • 而通过对源码的解读,可以发现被volatile修饰的变量存在于一个 lock; 的前缀。这个前缀实际上相当于是一个内存屏障,该屏障会为指令的执行提供如下几个保护:
    1. 确保指令重排序时不会将其后面的代码排到内存屏障之前;
    2. 确保指令重排序时不会将其前面的代码排到内存屏障之后;
    3. 确保在执行到内存屏障修饰的指令时前面的代码全部执行完成;
    4. 强制将线程工作内存中值的修改刷新至主内存中;
    5. 如果是写操作,会导致其它线程的工作内存中的缓存数据失效。

volatile的使用场景

  1. 开关控制利用。简单说就是if判断语句,当布尔变量修改时为了保证其它线程操作不会出现因数据不一致导致问题,用volatile修饰该变量,保证判断时该变量都是主内存中最新的值。
  2. 状态标记利用顺序性的特点。利用禁止重排序的特点。
  3. 在单例模式中的double-check的应用。这里用兴趣的朋友可以自行了解,它的主要作用就是保证对象的初始化一定成功。防止有为初始化成功的对象返回,从而可能会导致NPE,空指针异常。

volatile和synchronized的区别

  • 使用上的区别
    1. volatile关键字只能用于修饰实例变量或者类变量,不能用于修饰方法以及方法参数和局部变量、常量等;
    2. synchronized关键字不能用于对变量的修饰,只能用于修饰方法和代码块。
    3. volatile修饰的变量可以为null,但是synchronized关键字的锁对象不能为null
  • 对原子性的保证
    1. volatile不能保证原子性;
    2. synchronized是一种排他的机制,因此被其修饰的同步代码块无法被中途打断,因此其能够保证原子性。
  • 对可见性的的保证
    1. synchronized借助于JVM的指令monitor entermonitor exit对通过排他的方式使同步代码块串行化;在monitor exit时所有共享资源都会刷新到主内存中。
    2. volatile使用机器指令**lock;**的方式迫使其他线程工作内存中的数据缓存失效,不得不到主内存中重新获取。
  • 对有序性的保证
    1. volatile关键字禁止JVM编译器以及处理器对其进行重排序,所以它能保证有序性。
    2. synchroinzed是通过串行化的方式保证了有序性。但是在其修饰的方法或者代码块中依然可以进行指令重排序。
  • 其它
    • volatile不会使线程陷入阻塞;
    • synchronized会使线程进入阻塞状态。
  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值