Java基础-volatile

1、volatile的作用

  • 保证可见性
    • 使用 volatile 关键字会**强制将修改的共享变量的值立即写入主内存**。
    • 当修改volatile变量时,其他线程的工作内存上的缓存会失效,每次都从主存读取变量
    • 被volatile修饰的变量,在编译汇编指令是会**在前面添加lock前缀的命令**,cpu执行该指令时,会将高速缓存的数据刷新到主存并且通知其他缓存该值的缓存失效了,这就保证了可见性
  • 禁止指令重排
    • 编译层面:Java编译器不会对Java字节码进行指令重排序,在编译层面volatile变量只是添加一个标志
    • JVM层面:在volatile变量的写操作之前和读操作之后,会分别插入内存屏障,确保变量的读写操作在指令重排时的正确顺序
    • 汇编层面:内存屏障的是实现是通过cpu的LOCK指令实现的
  • 不保证原子性

2、可见性

在顺序性的基础上加上缓存一致性协议实现的

在volatile写后插入**store load屏障:保证写操作后续读操作** 全局可见

  • 1、强制处理器缓存回写到内存;
  • 2、触发缓存一致性协议发挥作用,使其他cpu核心的缓存失效,在读取是需要从主存读取

3、顺序性(禁止指令重排)

  • 3.1、指令重排(了解)
    • 重排序是指编译器和处理器为了优化程序性能而对指令序列进行重新排序的一种手段。
    • java语言规范规定JVM线程内部维持顺序化语义。即只要程序的最终结果与它顺序化情况的结果相等,那么指令的执行顺序可以与代码顺序不一致,此过程叫指令的重排序。
    • 指令重排序的意义是什么?JVM能根据处理器特性(CPU多级缓存系统、多核处理器等)适当的对机器指令进行重排序,使机器指令能更符合CPU的执行特性,最大限度的发挥机器性能。
    • 指令重排主要有两个阶段:
      • 1.编译器编译阶段:编译器加载class文件编译为机器码时进行指令重排
      • 2.CPU执行阶段: CPU执行汇编指令时,可能会对指令进行重排序
  • 3.2、JVM通过对volatile变量插入内存屏障来禁止指令重排;
    • 内存屏障:为了解决程序在运行过程中出现的**内存乱序访问问题,CPU或编译器在对内存随机访问的操作中的一个同步点**,使得此点之前的所有读写操作都执行后才可以开始执行此点之后的操作
    • 内存屏障的实现方式:
  • 3.3、禁止指令重排的行为
    • 读volatile,保证读后面的代码不会再读前执行(保证使用的变量是最新的)
    • 写volatile,保证写前面的代码不会再写后执行(保证写入是最新的)

4、原子性

不能保证原子性,可见性和顺序性,只是保证数据是最新的,仅仅是读写的一个限制,有些指令不知读写还包含了其他计算,这并不在volatile的作用范围内

6、实现方式

  • class字节码上增加volatile标志,没有其他额外标志
  • 运行时JIT编译器进行内存屏障插入
    • 读取volatile变量:读取前面插入**loadload屏障,读取后插入loadstore**屏障
    • 写入volatile变量:写入前面插入**storestore屏障,读取后插入storeload**屏障

5、优化

  • CPU工作时是和缓存进行交互,因为cpu、主存的速度差距太大,高速缓存防止主存拖累cpu。

  • 可以通过缓存行进行优化,cpu缓存中最小的缓存单位是缓存行,每个缓存行64个字节。如果保证每个volatile变量都在不同的缓存行中,则可以避免缓存行被其他volatile变量影响而频繁重新读取主存;

  • 一个volatile变量4个字节,追加15个变量即可实现

  • 不可优化情况:

    • 缓存行非64字节宽的处理器
    • 共享变量不会被频繁地写

JMM先天的“有序性”

happens-before 原则。如果两个操作的执行次序无法从happens-before原则推导出来,那么它们就不能保证它们的有序性,虚拟机可以随意地对它们进行重排序。

  • 程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
  • 锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作
  • volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
  • 传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
  • 线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
  • 线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
  • 线程终结规则:线程中所有的操作都先发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
  • 对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始