前面讲java关键字synchronized,我们知道在java中还有一大神器就是关键volatile,可以说是和synchronized各领风骚,其中奥妙。
synchronized 是阻塞式的同步,在线程竞争激烈的情况下回升级成为重量级锁。而volatile就可以说是java虚拟机提供的最轻量级的同步机制。
线程对 volatile 变量的修改会立刻被其他线程锁感知,既不会出现数据脏读的现象,从而保证了数据的“可见性” ! 被 volatile 修饰的变量能够保证没个线程能够获取变量的最新值,从而避免了数据脏读现象。
我们都知道在java 内存模型中,线程对数据的操作 ,不会直接去访问 主内存的 而是将 主内存的共享变量 “拷贝”
一份到 工作内存中 也就是说 主内存被所有线程共享,存放着 共享变量的“本尊”,而工作内存存储着共享变量的 “副本”。
线程对 共享变量的所有操作都必须在工作内存中进行,不能直接读写主内存中的变量,不同的线程之间也无法访问 彼此的工作内存,变量值的传递只能通过主内存来进行。
解释下 可见性
由于线程对共享变量的更改不会立刻同步到主内存中,而被volatile 修饰的变量。当一个线程修改了变量的值,新的值会立刻同步到主内存中去 ,这样就保证了不会出现脏读现象的发生。
为什么 volatile 关键字可以有这样的特向呢?
这得益于 java 语言的先行发生原则 【happens-before】
volatile 只能保证变量的 可见性,并不能保证变量的原则性。
在Java内存模型中,有main memory,每个线程也有自己的memory (例如寄存器)。为了性能,一个线程会在自己的memory中保持要访问的变量的副本。这样就会出现同一个变 量在某个瞬间,在一个线程的memory中的值可能与另外一个线程memory中的值,或者main memory中的值不一致的情况。 一个变量声明为volatile,就意味着这个变量是随时会被其他线程修改的,因此不能将它cache在线程memory中。
volatile 对 指令重排的影响?
指令重排?
是指jvm 在编译java 代码的时候或者 cpu 在执行 jvm 字节码的时候,对现有的指令顺序进行重新排序。
其目的是:
为了在不改变程序执行结果的前提下,优化程序的运行效率。【在这里所说的不改变执行结果,指的是不改变单线程下的程序执行结果】
为了解决指令重排就产生了 内存屏障
内存屏障共分为四种类型:
LoadLoad屏障:
抽象场景:Load1; LoadLoad; Load2
Load1 和 Load2 代表两条读取指令。在Load2要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
StoreStore屏障:
抽象场景:Store1; StoreStore; Store2
Store1 和 Store2代表两条写入指令。在Store2写入执行前,保证Store1的写入操作对其它处理器可见
LoadStore屏障:
抽象场景:Load1; LoadStore; Store2
在Store2被写入前,保证Load1要读取的数据被读取完毕。
StoreLoad屏障:
抽象场景:Store1; StoreLoad; Load2
在Load2读取操作执行前,保证Store1的写入对所有处理器可见。StoreLoad屏障的开销是四种屏障中最大的。
是不是感觉这些 内存屏障很抽象啊,其实我觉得
volatile做了什么?
在一个变量被 volatile 修饰后 ,jvm 会 为我 们做两件事:
1. 在每个 volatile 写操作之前 插入 StoreStore屏障,在写操作后插入 StoreLoad 屏障
2. 在每个volatile 读操作之前插入 LoadLoad 屏障,在读操作之后插入LoadStore屏障
从而阻止指令的重排序了。
内存屏障和 happends-before 之间有什么的联系呢?
happends-before[先行发生原则】 是 JSR-133 的规范之一;内存屏障是cpu 的指令,可以简单 认为前者是最终的目的,后者是实现的手段吧。
总结: volatile 有 两大特性
1. 保证变量在线程之间的可见性。 可见性是基于 cpu 的内存屏障指令,被 jsr-133 抽象为 happends-before 原则
2. 阻止编译时和运行期的指令重排。编译时 jvm 编译器遵循 内存屏障的约束,运行时依靠cpu内存屏障 指令来阻止重排.