对于volatile的解释,我相信更直白的说就是对于一个被volatile关键字修饰的变量,在并发情况下Java内存模型(JMM)保证每个线程对该变量的可见性,保证他们读取的数据是一致的,因此volatile实现了数据的可见性,有序性,但不保证原子性(下文会详细解释)。但是怎样保证可见性的呢?在jvm底层对于volatile修饰的共享变量进行写操作的时候主要实现了两个步骤:
Java程序执行时会编译为字节码通过加载器加载到JVM中,JVM执行字节码最终将其转变为汇编代码相关的CPU指令,因此对于使用该关键字修饰的变量,将其转变为汇编指令后比其他普通的变量多一行以Lock为前缀的指令,因此在对变量执行写操作的时候JVM会向处理器发送一条Lock#指令将缓存行中的数据更新到主存相应的地址中,但是此时的数据就会和其他处理器缓存行中的数据不一致,此时为了保证数据的一致性,就会实现缓存一致性协议,每一个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改过,就会将该缓存行设置为无效转态,当处理器需要读取该数据的时候,就会重新从主存中读取到缓存行。
简单理解也就是说,lock后就是一个原子操作。原子操作是指不会被线程调度机制打断的操作;这种操作一旦开始,就一直运行到结束,中间不会有任何 context switch (切换到另一个线程)。
当使用 LOCK 指令前缀时,它会使 CPU 宣告一个 LOCK# 信号,这样就能确保在多处理器系统或多线程竞争的环境下互斥地使用这个内存地址。当指令执行完毕,这个锁定动作也就会消失。
是不是感觉有点像Java的synchronized锁。但volatile底层使用多核处理器实现的lock指令,更底层,消耗代价更小。
因此有人将Java的synchronized看作重量级的锁,而volatile看作轻量级的锁 并不是全无道理。
lock前缀指令其实就相当于一个内存屏障。内存屏障是一组CPU处理指令,用来实现对内存操作的顺序限制。volatile的底层就是通过内存屏障来实现的。
系统的梳理一下volatile的实现原理:
并发的不可缺少的条件,对于volatile来说,它会使当前处理器缓存行的数据更新到内存,然后强制使其他处理器上存储该数据的缓存行失效,保证了数据的可见性与并发条件下的数据一致性。
对于有序性,volatile通过内存屏障来维护,硬件层的内存屏障主要分为两种Load Barrier,Store Barrier,即读屏障和写屏障。对于Java内存屏障来说,它分为四种,即这两种屏障的排列组合。
对于 i=1 这个赋值操作,由于其本身是原子操作,因此在多线程程序中不会出现不一致问题,但是对于 i++ 这种复合操作,即使使用 volatile 关键字修饰也不能保证操作的原子性,可能会引发数据不一致问题。
i++ 操作可以被拆分为三步:
1,线程读取 i 的值
2、i 进行自增计算
3、刷新回 i 的值
网上一些博客的解释是:
假设某一时刻 i=5,此时有两个线程同时从主存中读取了 i 的值,那么此时两个线程保存的 i 的值都是 5, 此时 A 线程对 i 进行了自增计算,然后 B 也对 i 进行自增计算,此时两条线程最后刷新回主存的 i 的值都是 6(本来两条线程计算完应当是 7)所以说 volatile 保证不了原子性。
我的不解之处:
既然 i 是被 volatile 修饰的变量,那么对于 i 的操作应该是线程之间是可见的啊,就算 A.,B 两个线程都同时读到 i 的值是 5,但是如果 A 线程执行完 i 的操作以后应该会把 B 线程读到的 i 的值置为无效并强制 B 重新读入 i 的新值也就是 6 然后才会进行自增操作才对啊。
后来参照其他博客终于想通了:
1、线程读取 i
2、temp = i + 1
3、i = temp
当 i=5 的时候 A,B 两个线程同时读入了 i 的值, 然后 A 线程执行了 temp = i + 1 的操作, 要注意,此时的 i 的值还没有变化,然后 B 线程也执行了 temp = i + 1 的操作,注意,此时 A,B 两个线程保存的 i 的值都是 5,temp 的值都是 6, 然后 A 线程执行了 i = temp (6)的操作,此时 i 的值会立即刷新到主存并通知其他线程保存的 i 值失效, 此时 B 线程需要重新读取 i 的值那么此时 B 线程保存的 i 就是 6,同时 B 线程保存的 temp 还仍然是 6, 然后 B 线程执行 i=temp (6),所以导致了计算结果比预期少了 1。