在了解valotile关键字之前。我们先来了解其他相关概念。
1.1 java内存模型:
不同的平台,内存模型是不一样的,我们可以把内存模型理解为在特定操作协议下,对特定的内存或高速缓存进行读写访问的过程抽象。Java 虚拟机规范中试图定义一种 Java 内存模型(Java Memory Model,简称 JMM)来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让 Java 程序在各种平台下都能达到一致的内存访问效果,不必因为不同平台上的物理机的内存模型的差异,对各平台定制化开发程序。
更具体一点说,Java 内存模型提出目标在于,定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量这样的底层细节。
1.2 Java 内存模型的组成
主内存
Java 内存模型规定了所有变量都存储在主内存(Main Memory)中(此处的主内存与介绍物理硬件的主内存名字一样,两者可以互相类比,但此处仅是虚拟机内存的一部分)。
工作内存
每条线程都有自己的工作内存(Working Memory,又称本地内存.),线程的工作内存中保存了该线程使用到的变量的主内存中的共享变量的副本拷贝。
(工作内存是 JMM 的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。)
JAVA内存模型抽象示意图:
线程执行的时候,将首先从主内存读值,再load到工作内存中的副本中,然后传给处理器执行,执行完毕后再给工作内存中的副本赋值,随后工作内存再把值传回给主存,主存中的值才更新。
在这个过程中如果出现多个线程同时在处理这些值,岂不是会出现并发问题?
1.3 Java 内存间的交互操作
关于主内存与工作内存之间的具体交互协议,即一个变量如何从主内存拷贝到工作内存、如何从工作内存同步回主内存之类的实现细节,Java 内存模型中定义了下面 8 种操作来完成。
虚拟机实现时必须保证下面介绍的每种操作都是原子的,不可再分的(对于 double 和 long 型的变量来说,load、store、read、和 write 操作在某些平台上允许有例外)。
8 种基本操作,如下图:
lock (锁定) , 作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
unlock (解锁) , 作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
read (读取) , 作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的 load 动作使用。
load (载入) , 作用于工作内存的变量,它把 read 操作从主内存中得到的变量值放入工作内存的变量副本中。
use (使用) , 作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时就会执行这个操作。
assign (赋值) , 作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
store (存储) , 作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后 write 操作使用。
write (写入) , 作用于主内存的变量,它把 Store 操作从工作内存中得到的变量的值放入主内存的变量中。
Java 内存模型运行规则
内存交互基本操作的 3 个特性
Java 内存模型是围绕着在并发过程中如何处理这 3 个特性来建立的.
原子性(Atomicity)
原子性,即一个操作或者多个操作要么全部执行并且执行的过程不会被任何因素打断,要么就都不执行。
即使在多个线程一起执行的时候,一个操作一旦开始,就不会被其他线程所干扰。
可见性(Visibility)
可见性是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。
正如上面“交互操作流程”中所说明的一样,JMM 是通过在线程 1 变量工作内存修改后将新值同步回主内存,线程 2 在变量读取前从主内存刷新变量值,这种依赖主内存作为传递媒介的方式来实现可见性。
有序性(Ordering)
在Java内存模型中,允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性
Valotile的作用?
1)保证可见性
valotile本意是不稳定的,易挥发的,也就是说,用它修饰的变量是可变的。
结合我们的Java内存模型介绍,我们可以了解在多线程环境下,线程会将线程间共享的变量保存在本地内存中,而不是从内存中读取,这就可能会引发不一致的问题,另一个进程可能在此线程运行期间改变了变量的值,而此线程并没有看到变化。
而volatile修饰的成员变量在每次被线程访问时,都强迫从共享内存中重读该成员变量的值。而且,当成员变量发生变化时,强迫线程将变化值回写到共享内存。这样在任何时刻,两个不同的线程总是看到某个成员变量的同一个值。
Java语言规范中指出:为了获得最佳速度,允许线程保存共享成员变量的私有拷贝,而且只当线程进入或者离开同步代码块时才与共享成员变量的原始值对比。
也就是说,JVM会对线程变量的访问进行优化,这样当多个线程同时与某个对象时交互,就必须要注意到要让线程及时的得到共享成员变量的变化。
而volatile关键字就是提示VM:对于这个成员变量不能保存它的私有拷贝,而应直接与共享成员变量交互。
使用valotile前后示意图:
从上图可以直观的看到valotile的实现可见性的原理,线程对变量读取的时候,直接从主内存中读,而不是从线程的工作内存。也就避免了其他线程操作时修改了变量的值。
2)禁止进行指令重排序
为了使得处理器内部的运算单元尽量被充分利用,提高运算效率,处理器可能会对输入的代码进行排序。但是可能出现如下情况:
正常情况下,逻辑 A 执行完之后再执行逻辑 B。在处理器乱序执行优化情况下,有可能导致 flag 提前被设置为 true,导致逻辑 B 先于逻辑 A 执行。
这个时候volatile又起到了作用。
当程序执行到 volatile 变量的读操作或者写操作时, 在其前面的操作的更改肯定全部已经进行,且结果已经对后面的操作可见;在其后面的操作肯定还没有进行。
在进行指令优化时, 不能将在对 volatile 变量访问的语句放在其后面执行,也不能把 volatile 变量后面的语句放到其前面执行。
(普通的变量仅仅会保证该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证赋值操作的顺序与程序代码中的执行顺序一致)
实现原理:
内存屏障会提供3个功能:
1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2)它会强制将对缓存的修改操作立即写入主存;
3)如果是写操作,它会导致其他CPU中对应的缓存行无效。
总结:1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。
2)禁止进行指令重排序。