Java关键字—volatile

Java关键字—volatile

JMM

Java内存模型简称JMM(Java Memory Model),是Java虚拟机所定义的一种抽象规范,用来屏蔽不同硬件和操作系统的内存访问差异,让java程序在各种平台下都能达到一致的内存访问效果。

Java内存模型将内存分为主内存(存放共享变量,比如静态变量、堆内存实例)和工作内存(存放线程需要使用的变量,里面有对共享变量的拷贝)。线程对共享变量的所有操作都必须先在工作内存进行,不能直接改变主内存的变量。不同线程之间也无法访问彼此的工作内存,变量值的传递只能通过主内存来进行。
在这里插入图片描述
我们通常说的线程同步,也就是线程间对共享变量的处理结果要明确,不能出现一个线程在读取变量值的同时另一个线程在写该变量值。

比如定义一个静态变量 static int t = 0,在主内存中 t = 0。现在要线程 A 执行 **t = 3;**这个赋值语句, 那么在JMM中的流程如下:

①线程 A 的工作内存先建立一份 t 的拷贝,也就是线程 A 的工作内存中的 t 初值为0;
②线程 A 将 t 值修改为3,即线程 A 的工作内存中 t = 3,但主内存中 t 仍然等于 0;
③线程 A 将更新的 t 值同步到主内存中,即线程 A 的工作内存和主内存中 t = 3。

假如此时有线程 B 要打印静态变量 t 的值,由于线程 B 只能访问主内存中的 t ,如果线程 B 打印时线程 A 执行完了①②③步,那么线程 B 打印 t = 3;但假如线程 B 打印时线程 A 只执行了①②步,那么线程 B 会读取到主内存中未更新的 t 值,从而打印 t = 0

volatile特性

  1. 变量可见性

    使用 volatile 修饰的共享变量对所有线程都可见,即 一个线程修改了共享变量的值,新的值会立刻同步到主内存中,而其他线程读取这个变量时,也会从主内存中读取最新的变量值

    值得注意的是, volatile 修饰的变量并不会保证线程安全,因为 volatile 只能保证变量在读取时是最新值,但如果对变量进行修改的操作不具有原子性,则该变量值的更新仍然可能“慢人一步”。

    比如对静态变量的 自加操作 ++,在字节码层面可以拆分成如下指令:

    getstatic //读取静态变量
    iconst_1 //定义常量1
    iadd //静态变量增加1
    putstatic //结果同步到主内存中

    若10个线程分别对某个初值为 0 的静态变量自加操作 10 次,那么最后的结果可能小于 100,因为 volatile 只能保证每个线程每次读取时的值是最新的,但如果在线程 A 进行自加操作时,其他线程完成了自加操作,更新了该静态变量的值,此时线程 A 中的值并不会随之更新,而当线程 A 完成自加操作后,更新主内存中的静态变量值,仍然是旧值+1,会覆盖掉其他进程 自加操作 的结果,因此最后的结果可能小于 100 。这也是为什么 volatile 修饰的变量并不会保证绝对的线程安全。

  2. 禁止指令重排

    指令重排是指JVM在编译Java代码的时候,或者CPU在执行JVM字节码的时候,对现有的指令顺序进行重新排序。

    指令重排的目的是为了在不改变程序执行结果的前提下,优化程序的运行效率。需要注意的是,这里所说的不改变执行结果,指的是不改变单线程下的程序执行结果,但有可能影响多线程的执行结果,例子详见 设计模式—单例模式

    volatile 通过 内存屏障 来禁止指令重排。

    内存屏障也称为内存栅栏或栅栏指令,是一种屏障指令,它使CPU或编译器对屏障指令之前和之后发出的内存操作执行一个排序约束。 这通常意味着在屏障之前发布的操作被保证在屏障之后发布的操作之前执行。

    内存屏障共分为四种类型

    LoadLoad 屏障:抽象场景:Load1; LoadLoad; Load2Load1 和 Load2 代表两条读取指令。在Load2要读取的数据被访问前,保证Load1要读取的数据被读取完毕。

    StoreStore 屏障:抽象场景:Store1; StoreStore; Store2Store1 和 Store2代表两条写入指令。在Store2写入执行前,保证Store1的写入操作对其它处理器可见。

    LoadStore 屏障:抽象场景:Load1; LoadStore; Store2在Store2被写入前,保证Load1要读取的数据被读取完毕。

    StoreLoad 屏障:抽象场景:Store1; StoreLoad; Load2在Load2读取操作执行前,保证Store1的写入对所有处理器可见。StoreLoad屏障的开销是四种屏障中最大的。

    volatile 操作

    1.在每个 volatile 写操作前插入 StoreStore 屏障,在写操作后插入 StoreLoad 屏障。
    2.在每个 volatile 读操作前插入 LoadLoad 屏障,在读操作后插入 LoadStore 屏障。

    这样就可以阻止 JVM 的重排序操作。

  3. long 类型和 double 类型的8字节赋值问题

    Oracle Java Spec里面可以看到:

  • 对于64位的long和double,如果没有被volatile修饰,那么对其操作可以不是原子的。在操作的时候,可以分成两步,每次对32位操作
  • 如果使用volatile修饰long和double,那么其读写都是原子操作。
  • 对于64位的引用地址的读写,都是原子操作。
  • 在实现JVM时,可以自由选择是否把读写long和double作为原子操作。
  • 推荐JVM实现为原子操作 。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值