快捷搜索:  汽车  科技

关键字volatile举例(Volatile深度剖析-指令重排序)

关键字volatile举例(Volatile深度剖析-指令重排序)2. 内存屏障(Memory Fence)1. Happens-before原则2、计算机处理器层:lock指令那问题来了,Volatile是如何保证变量读和写的呢?我们先来了解两个概念

关键字volatile举例(Volatile深度剖析-指令重排序)(1)

前言

回顾下上一篇文章《Volatile深度剖析-可见性》,我们知道了volatile是如何保证其可见性的,简单来说有两点

1、JMM层

在jvm虚拟机栈中,每一个线程运行时都有一个线程栈,线程栈保存了线程运行时候变量值信息。当线程访问某一个对象的时候,首先通过对象的引用找到对应在堆内存的变量的值,然后把堆内存变量的具体值load到线程本地内存中,建立一个变量副本,之后线程就不再和对象在堆内存变量值有任何关系,而是直接修改副本变量的值,在修改完之后的某一个时刻(线程退出之前),自动把线程变量副本的值回写到对象在堆中变量。这样在堆中的对象的值就产生变化了

2、计算机处理器层:lock指令

那问题来了,Volatile是如何保证变量读和写的呢?

我们先来了解两个概念

1. Happens-before原则

2. 内存屏障(Memory Fence)

Happens-before原则(先行发生原则)

先行发生原则说的是Java内存模型中两个操作之间的执行顺序关系。

举个栗子

int a = 1 操作A int b = a 操作B a = 2

假设操作A先于操作B发生,意味着a=1的赋值影响到b=a=1的赋值。在单线程环境下,java线程天生就是有序执行的;但是在多线程环境下,b未必等于1,有可能为2。

为什么呢?

在上一篇文章《Volatile深度剖析-可见性》我们讲过两个概念:线程的工作内存和主内存。也就是说,多线程环境下,如果一个线程更新了主内存,会导致其他线程工作内存的共享变量失效,那么当其他线程检测到工作内存中共享变量的值失效时,会去主内存重新拷贝新值到工作内存中。

为什么在多线程环境下,程序执行会看似无序呢?

因为不符合Java内存模型的“先行发生原则”规范。

先行发生原则规则如下:

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

那怎么保证先行发生原则呢?

这里涉及到一个内存屏障指令(Memory Fence)的概念。

内存屏障指令(Memory Fence)

内存屏障是一种CPU指令,是指重排序时不能把后面的指令重排序到内存屏障之前的位置。

是不是听了很晕?

白话来讲就是往你的一些代码里面动态插入一些屏障指令,目的是防止在多线程环境下,变量赋值错乱问题。

是不是好像明白点了?

这个就好比你家种的白菜,你是不是会担心白菜叶子会被鸡吃掉,那你会怎么做呢?想到什么了?对,用围栏把菜园子围起来,这样可以防止鸡偷吃白菜叶子。

常见的内存屏障分为以下几种:

LoadLoad屏障指令:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。

StoreStore屏障指令:对于这样的语句Store1; StoreStore;Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。

LoadStore屏障指令:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。

StoreLoad屏障指令:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。

说了这么多,其实就是想说明为什么volatile要禁止指令重排序。

指令重排序

先说一个概念有序性

即程序执行的顺序按照代码的先后顺序执行。

Java程序中,如果是本线程内,所有操作都是有序的;如果是多线程环境下,则是无序的。前半句指的是线程内表现为串行语义,后半句指的是“指令重排序”和“工作内存与主内存同步延迟” 现象。

Java语言提供了volatile和synchronized两个关键字来保证线程之间的有序性

1、volatile关键字本身就包含了指令重排序的语义2、synchronized由一个变量在同一时刻只允许一个线程对其进行lock操作,这条规定的获得,规定了持有同一个锁的两个同步块只能串行地进入。

栗子奉上

//线程1执行的代码 int i = 0; i = 10; //线程2执行的代码 j = i;

上面代码定义了一个int型变量,定义了一个boolean类型变量,然后分别对两个变量进行赋值操作。从代码顺序上看,语句1是在语句2前面的,那么JVM在真正执行这段代码的时候会保证语句1一定会在语句2前面执行吗?不一定,为什么呢?这里可能会发生指令重排序(Instruction Reorder)。

什么是指令重排序?

一般来说,处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的。

比如上面的代码中,语句1和语句2谁先执行对最终的程序结果并没有影响,那么就有可能在执行过程中,语句2先执行而语句1后执行。但是要注意,虽然处理器会对指令进行重排序,但是它会保证程序最终结果会和代码顺序执行结果相同,那么它靠什么保证的呢?再看下面一个例子,代码清单如下:

int a = 10; //语句1 int r = 2; //语句2 a = a 3; //语句3 r = a*a; //语句4

这段代码有4个语句,那么可能的一个执行顺序是:

语句2 > 语句1 > 语句3 > 语句4

那么可不可能是这个执行顺序呢:

语句2 > 语句1 > 语句4 > 语句3

不可能,因为处理器在进行重排序时是会考虑指令之间的数据依赖性,如果一个指令Instruction 2必须用到Instruction 1的结果,那么处理器会保证Instruction 1会在Instruction 2之前执行。

虽然重排序不会影响单个线程内程序执行的结果,但是多线程呢? 代码清单如下:

//线程1: context = loadContext(); //语句1 inited = true; //语句2 //线程2: while(!inited ){ sleep() } doSomethingwithconfig(context);

上面代码中,由于语句1和语句2没有数据依赖性,因此可能会被重排序。假如发生了重排序,在线程1执行过程中先执行语句2,而此是线程2会以为初始化工作已经完成,那么就会跳出while循环,去执行doSomethingwithconfig(context)方法,而此时context并没有被初始化,就会导致程序出错。

从上面可以看出,指令重排序不会影响单个线程的执行,但是会影响到线程并发执行的正确性

也就是说,要想并发程序正确地执行,必须要保证原子性、可见性以及有序性。只要有一个没有被保证,就有可能会导致程序运行不正确。

所以,基于以上论述,volatile为了保证可序性,就必须要禁止指令重排序。

Volatile使用场景

1. 状态标识

2. 双重检查(Double-Check) ,JDK1.5之后才稳定下来

需要注意的是volatile无法替代synchronized的。因为volatile关键字无法保证操作的原子性。通常来说,使用volatile必须具备以下2个条件:

1)对变量的写操作不依赖于当前值

2)该变量没有包含在具有其他变量的不变式中

哪怎么保证volatile操作的原子性呢?

参考

《深入理解Java虚拟机》

https://www.cnblogs.com/chenyangyao/p/5269622.html

大家好,我是Wooola,10年JAVA老兵,擅长微服务,分布式,并发,工作流。请大家多多关注我。

猜您喜欢: