您的位置:首页 > 编程语言 > Java开发

JAVA多线程技术学习笔记——volatile关键字解析

2017-06-13 10:26 330 查看

JAVA多线程技术学习笔记——volatile关键字解析


一、volatile关键字的作用

volatile关键字的作用主要有两个确保有序性禁止指令重排,确保可见性。简单的来说就是保护变量不被主函数和中断函数反复修改造成读写错误


1.禁止指令重排

a)什么是指令重排

我们知道,代码的执行顺序一般来说是从上之下,即拥有我们所称之为有序性。看下面一段代码:

int i = 0;
boolean flag = false;
i = 1;                //语句1
flag = true;          //语句2


通常语句1是在语句2前面的,但是JVM在真正是按照这样的顺序运行的吗?其实并不一定,因为JVM可能会对代码进行指令重排。所谓的指令重排就是由于处理器为了提高运行效率可能会在确保输出结果与代码按顺序执行的结果相同的前提下调整代码的运行顺序从而优化代码。语句1和语句2谁先执行对最终的程序结果并没有影响,那么就有可能在执行过程中,语句2先执行而语句1后执行。但是要注意,虽然处理器会对指进行重排序,但是它会保证程序最终结果会和代码顺序执行结果相同,那么它靠什么保证的呢?这里就要引入一个原则——happens-before原则,只要符合该原则的代码都不能调换顺序具有依赖性,不符合的代码就可能进行指令重排。

  下面是happens-before原则的具体内容(先行发生原则):
程序次序规则:一个线程内,按照代码顺序,书写在前面的操作先行发生于书写在后面的操作
锁定规则:一个unLock操作先行发生于后面对同一个锁额lock操作
volatile变量规则:对一个变量的写操作先行发生于后面对这个变量的读操作
传递规则:如果操作A先行发生于操作B,而操作B又先行发生于操作C,则可以得出操作A先行发生于操作C
线程启动规则:Thread对象的start()方法先行发生于此线程的每个一个动作
线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生
线程终结规则:线程中所有的操作都先行发生于线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值手段检测到线程已经终止执行
对象终结规则:一个对象的初始化完成先行发生于他的finalize()方法的开始
  这8条原则摘自《深入理解Java虚拟机》。

b)防止指令重排的作用

虽然指令重排不会影响单线程的执行结果,那么在多线程中时不是也是这样的呢?
下面是我们的例子:
//线程1:
context = loadContext();   //语句1
inited = true;             //语句2

//线程2:
while(!inited ){
sleep()
}
doSomethingwithconfig(context);

由上面的代码可以看出线程一中的语句1和语句2是没数据依赖性的,所以他们可能会发生指令重排,那么可怕的事情就发生了,程序率先执行了语句二,线程二中的inited被设置
为true导致没有执行sleep方法而是去执行了doSomeThingWithConfig方法,而此时的context还未被初始化,那么就出现了程序异常。所以我们需要限制JVM的指令重排动作以确
保程序能正常获得执行结果。


2.使得操作可见


a)什么是操作可见性

可见即可以看见的,可见性是指在一个线程中修改了数据其他线程也能同步看见这个数据的修改。


b)有什么作用

在对数据的存取的操作上,为了提高效率,各单位在对数据进行修改等操作并不是直接在内存中修改的,而是在高速缓存(可理解为自己的内存)中修改的,之后再将修改的数据同步上去。这样就使得写操作变成了两步,1.读取内存中的输出存入告诉缓存;2.修改高速缓存中的数据;3.将高速缓存数据刷入内存。这个时候就会存在一个问题:假如当a执行了+1操作之后还没来得及将数据刷入内存,cpu就转去执行b,而b对数据也执行了+1操作,讲数据输入内存中,之后再执行a的刷入数据操作,结果本来两个操作+2,现在只+1。所以我们需要所以对数据的修改是各个线程可见的。
volatile关键字就是强制单位在对数据进行操作时读取内存中的数据而不是在高速缓存中的数据。即使得其他存有该对象副本的高速缓存失效,从而强迫其他想要对该对象进行操作的单位去内存中获取新的修改后的值。


三、关于volatile关键字的应用


1.volatile能否保证原子性

大部分情况下volatile关键字只有确保操作可见性,顺序性的功能,不能确保操作的原子性,但是也有一些特例,一个典型的例子是在类中有一个 long类型的成员变量。如果你知道该成员变量会被多个线程访问,如计数器、价格等,你最好是将其设置为 volatile。因为 Java中读取 long 类型变量不是原子的,需要分成两步,如果一个线程正在修改该 long变量的值,另一个线程可能只能看到该值的一半(前 32 位)。但是对一个 volatile型的 long 或 double 变量的读写是原子。

2.修饰数组,修饰对象会有怎样的效果

对于数组对象而言,我们使用volatile关键字修饰只能影响对数组对象的应用操作,而不会影响到数组对象的内部成员的操作。

3.volatile能否确保线程的安全

不能,上文也说明了volatile对象的作用只有两个,仅仅是synchronized的一部分,不是真正意义上的线程安全。所以volatile主要针对的是保护变量不被主函数和中断函数反复修改造成读写错误
内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息
标签: