Java并发编程_volatile可见性底层实现原理

JMM(java memory model)

java内存模型主要目标是定义程序中的变量,(此处所指的变量是实例字段、静态字段等,不包含局部变量和函数参数,因为这两种是线程私有无法共享)在虚拟机中存储到内存与从内存读取出来的规则细节,Java 内存模型规定所有变量都存储在主内存中,每条线程还有自己的工作内存,工作内存保存了该线程使用到的变量到主内存副本拷贝,线程对变量的所有操作(读取、赋值)都必须在自己的工作内存中进行而不能直接读写主内存的变量,不同线程之间无法相互直接访问对方工作内存中的变量,线程间变量值的传递均需要在主内存来完成。

JMM数据原子操作:
lock:作用于主内存变量,把一个变量标识为一条线程独占状态。
unlock:作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
read:作用于主内存变量,把一个变量从主内存传输到线程的工作内存中,以便随后的 load 动作使用。
load:作用于工作内存变量,把 read 操作从主内存中得到的变量值放入工作内存的变量副本中。
use:作用于工作内存变量,把工作内存中的一个变量值传递给执行引擎,每当虚拟机遇到一个需要使用变量值的字节码指令时执行此操作。
assign:作用于工作内存变量,把一个从执行引擎接收的值赋值给工作内存的变量,每当虚拟机遇到一个需要给变量进行赋值的字节码指令时执行此操作。
store:作用于工作内存变量,把工作内存中一个变量的值传递到主内存中,以便后续 write 操作。
write:作用于主内存变量,把 store 操作从工作内存中得到的值放入主内存变量中。

下面以程序示例进行分析:

public class VolatileTest {
     //添加volatile关键字
	private static volatile  boolean  initFlag=false;
	 //不添加volatile关键字
	//private static  boolean initFlag=false;
	
	public static void main(String[] args) throws InterruptedException {
		//线程1
		new Thread(new Runnable() {
			@Override
			public void run() {
				System.out.println("线程1开始执行...");
				while(!initFlag){
					
				}
				System.out.println("线程1执行结束...");
			}
		}).start();
		
		Thread.sleep(2000);
		//线程2
		new Thread(new Runnable() {
			@Override
			public void run() {
				System.out.println("线程2开始执行...");
				initFlag=true;
				System.out.println("线程2修改initFlag为true,线程结束...");
			}
		}).start();
	}
}

(1) 如果不加volatile关键字,那么控制台输出结果为:
线程1开始执行…
线程2开始执行…
线程2修改initFlag为true,线程结束…

内存模型及流程示意图:
在这里插入图片描述
分析:
线程2对主内存中的数据进行更新后线程1并不知道, 也没有去更新数据, 所以出现了JMM缓存不一致问题

(2) 如果添加volatile关键字,那么控制台输出结果为:
线程1开始执行…
线程2开始执行…
线程2修改initFlag为true,线程结束…
线程1执行结束…

内存模型及流程示意图:
在这里插入图片描述
volatile缓存可见性实现原理:
底层实现主要是通过汇编lock前缀, 它会锁定这块内存区域的缓存(缓存行锁定) 并回写到主内存
IA-32结构软件开发者手册对lock指令的解释:
(1) 会将当前处理器缓存行的数据立即写回到系统内存
(2) 这个写回内存的操作会引起其他CPU中缓存了该内存地址的数据无效(MESI)

MESI缓存一致性协议:
多个CPU从主内存中获取同一个数据到各自的高速缓存, 当其中某个CPU修改了缓存中的数据, 该数据会马上同步回主内存, 其它CPU通过总线嗅探机制可以感知到数据的变化, 从而将自己缓存中的数据失效

分析:
当线程2的initFlag变量被重写回主内存, 在经过主线时线程1的CPU总线嗅探机制会监听到此信息,会让线程1工作内存中的initFlag变量失效, 迫使线程1去更新数据; 并且线程2在写回主内存时进行加锁, 防止在更新的过程中有其他线程来获取数据, 由于线程2更新还未完成, 而造成其他线程获取的数据仍为之前的数据; 因此可以通过volatile关键字来解决缓存可见性问题

注意:

(1) Volatile不能保证原子性:
在这里插入图片描述
分析:
线程1和线程2都对工作内存中的num进行加一操作之后, 开始对主内存中的num进行更新, 在执行store方法之前会进行lock, 如果线程1抢先执行lock, 那么线程2则无法再执行lock; 并且当数据重写进入主线时, 线程2的CPU总线嗅探机制会使线程2中的num失效, 这会导致线程2所执行的num++操作无效, 所以可以看出Volatile不能保证原子性

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

(2) Volatile可以保证有序性

在指令重排时在volatile指令前后的JVM指令不允许重排(volatile指令前的指令不允许冲到volatile指令后)

以上为个人理解, 如果有不对的地方, 欢迎指出!

推荐一篇比较好的博文:
https://www.cnblogs.com/noKing/p/9190041.html

  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值