Volatile概念
volatile是一个特征修饰符(type specifier)。volatile的作用是作为指令关键字,确保本条指令不会因编译器的优化而省略,且要求每次直接读值。volatile的变量是说这变量可能会被意想不到地改变,这样,编译器就不会去假设这个变量的值了。——百度百科
所以呢它主要是两个作用:一个是线程可见(保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。),一个是防止指令重排序。要理解这些首先呢需要了解我们java的一个内存模型(Java Memory Model,JMM)
Java Memory Model
我们知道在java中,实例域、静态域和数组元素都存储在堆内存中,堆内存是线程共享,而其他的一些虚拟机栈等它们的的一些内容是线程独占不会有内存可见的问题也不受内存模型影响。Java线程之间的通信由Java内存模型控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。Java内存模型的抽象示意图如下:
线程从主内存拿取到某以变量到自己本地内存进行操作,完毕之后再将新的值覆盖到主内存。之后再有其他线程拿到此变量得到一个新的值。通过这样的方式达到了一个不同线程之间的通讯,而且这个通信过程必须要经过主内存。JMM通过控制主内存与每个线程的本地内存之间的交互,来为java程序员提供内存可见性保证。
线程可见
当一个线程修改了共享变量的值,其他线程能够立即得知这个修改,这样的方式来保证单次读写操作的同步性。
例子1:j的值会是多少呢?
// 线程A执行的代码k = 5;//线程B执行的代码int j = k;
答案是无法确定。因为即使线程A已经把k的值更新为5,但是这个操作是在线程A的本地内存中完成的,本地内存所更新的变量并不会立即同步回主内存,因此线程B从主内存中得到的变量k的值是不确定的。这就是可见性问题,线程A对变量k修改了之后,线程B没有立即看到线程A修改的值。
例子2: 新线程会打印出end么?
public class Test { private static /*volatile*/ boolean flag = true public static void main (String[] args) throws I interrupted Exception { new Thread(()-> { while (flag) { //do sth } System•out•println("end"); },name: "server") .start(); Thread.sleep( millis: 1GGG); flag = false }}
答案是不会,新线程的本地内存拿到的flag是true,它一直使用的就是true。即使主线程已经将flag更改并同步到了主内存。新线程的本地空间已经有了flag也不会再去主内存取了。这时使用volatitle关键字修饰该变量就可以保证变量更改进行立刻同步,并且其他地方使用该变量每次都要重新从主内存拿取。
通过两个例子大概可以知道的是volatile修饰的变量,变动会及时更新并且线程都会去主内存取而不是到本地
指令重排序
实际上就是在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序。
编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。
指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
内存系统的重排序。由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
这里先用一个列子说明重排序的存在,看下面伪代码:
a=0,b=0,x=0,y=0
//线程一a=1;x=b;
//线程二b=1;y=a;
假如不会发生重排序。那么执行过程中两个线程四条指令至少a=1一定在x=b之前,b=1一定在y=a之前。执行顺序可能出现六种情况
//情况一:(串)线程一执行完后线程二才开始a=1;x=b;b=1;y=a;//结果x=0,y=1//情况二:(串)线程一开始时线程二已执行完b=1;y=a;a=1;x=b;//结果x=1,y=0
//情况三:(并)两线程交叉执行a=1;b=1;x=b;y=a;//结果x=1,y=1//情况四:(并)两线程交叉执行b=1;a=1;y=a;x=b;//结果x=1,y=1//情况五:(并)线程一执行中途线程二开始并执行完a=1;b=1;y=a;x=b;//结果x=1,y=1//情况五:(并)线程二执行中途线程一开始并执行完b=1;a=1;x=b;y=a;//结果x=1,y=1
在不会被调整顺序的情况中结果无非三种(1,0)、(0,1)、(1,1)但实际结果会出现x=0,y=0。也就是说线程的指令是乱序的会进行调整。对于单线程来说调整是不会影响结果的只是提升了效率比如省略一加一减相互抵消的指令或者调整顺序,最后结果不影响。
/*下面这三组就不会发生指令重排因为改了顺序就会影响结果*/a=1;b=a;a=1;a=2;a=b;b=1;
在上面双线程的例子中无论是线程一的a=1,x=b还是线程二的b=1,y=a。在它们本线程中两条语句并不是依赖的,所以调换不影响结果所以会出现调换。但两个线程放一起变量是依赖的,最后因为重排导致结果不一致。所以在多线程中往往会出现问题所以需要禁止重排,使用volatile那么指令之间加入内存屏障指令就可以禁止重排。
本文分享自微信公众号 - IT那个小笔记(qq1839646816)。
如有侵权,请联系 support@oschina.cn 删除。
本文参与“OSC源创计划”,欢迎正在阅读的你也加入,一起分享。