原创

Volatile关键字与线程安全

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接:https://localhost01.blog.csdn.net/article/details/78172827

Volatile关键字与线程安全

volatile关键字,它的语义有二:
1.volatile修饰的变量对于其他线程具有立即可见性
即被volatile修饰的变量值发生变化时,其他线程可以立马感知。而对于普通变量,值发生变化后,需要经过store、write过程将变量从当前线程的工作内存写入主内存,其他线程再从主内存通过read、load将变量同步到自己的工作内存,由于以上流程时间上的影响,可能会导致线程的不安全。
当然要说使用volatile修饰过的变量是线程安全的,也不全对。因为volatile是要分场景来说的:如果多个线程操作volatile修饰的变量,且此时的“操作”是原子性的,那么是线程安全的,否则不是。如:
volatile int i=0;
线程1执行: for(;i++;i<100);
线程2执行: for(;i++;i<100);
最后 i 的结果不一定会是200(线程不安全),因为i++操作不是原子性操作,它涉及到了三个子操作:从主内存取出i、i+1、将结果同步回主内存。那么就有可能一个线程拿到最新值,正开始执行第二个子操作,而值还未来得及改变时,第二个线程就已经拿到同样的值开始执行第二个子操作了。这样一来,就有可能两个线程给同一个值加了一次1,所以就算有volatile修饰也是无力回天。
这时,我们应该使用synchronize或concurrent原子类来保证“操作”的原子性。故volatile的使用场景应该是:修饰的变量的有关操作都是原子性的时候。比如修饰一个控制标志位:
volatile boolean tag=true;
线程1 while(tag){};
线程2 while(tag){};
当tag=false时,两个线程都能马上感知到并停止while循环,因为简单的赋值语句属于原子操作(赋予具体的值而不是变量),它只负责把主内存的tag同步为true。能实现可见性的关键字除了volatile,还有synchronize与final,synchronize是因为变量执行解锁操作前,会把变量同步到主内存(自带可见性);而final则是被其修饰的变量一旦初始化,且构造器没有把this引用传递到外面去的情况下,其他线程就可以看见它的值(因为它永不发生变化)。
2.禁止指令重排序
new一个对象可以分解为如下的3行伪代码
memory=allocate(); //1:分配对象的内存空间
ctorInstance(memory); //2:初始化对象
instance=memory; //3:设置instance指向刚分配的内存地址
上面3行代码中的2和3之间,可能会被重排序(在一些JIT编译器上,这种重排序是真实发生的)。2和3之间重排序之后的执行时序如下
memory=allocate(); //1:分配对象的内存空间
instance=memory; //3:设置instance指向刚分配的内存地址,注意此时对象还没有被初始化
ctorInstance(memory); //2:初始化对象
如果发生重排序,另一个并发执行的线程B就有可能在还没初始化对象操作前就拿走了instance,但此时这个对象可能还没有被线程真正初始化,因此这是线程不安全的。
Java先行发生原则:
1.程序次序规则:在一个线程内,按照程序代码顺序(准确说应是控制流顺序),先写的先发生,后写的后发生。
2.管程锁定规则:一个解锁操作先于后面对该锁的锁定操作。
3.volatile变量规则:对一个volatile变量的写操作先行发生于后面对这个变量的读操作。
4.线程启动规则:线程对象的start()方法先行发生于此线程的每一个动作。
5.线程终止规则:线程的所有操作都先于此线程的终止检测。
6.线程中断规则:对线程interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生。可通过Thread.interrupted()方法检测是否会有中断将发生。
7.对象终结规则:一个对象的初始化发生先行于它的finalize()方法的开始。
8.传递性:如果操作A先于B,B先于C,那么A先于C。
例:
private int value=0;
public void setValue(int value){
this.value=value;
}
public int getValue(){
return this.value;
}
如果线程1调用setValue(1)方法,线程2调用getValue(),那么得到的value是0还是1呢,这是不确定的。因为它不满足上面的先行发生原则:因为不是在一个线程,所以不符合程序次序规则;因为没有同步块,也就不存在加锁和解锁,因此也不符合管程锁定规则;没有volatile修饰,也就不存在volatile变量规则,更没有后面的线程相关规则和传递性可言。针对此,可做以下修改:
将上面的setter、getter方法都用synchronize修饰,使其满足管程锁定规则;或者使用volatile修饰,因为setValue()是基本的赋值操作,属于原子操作,因此符合volatile的使用场景。
注:
1.从上总结,线程安全一般至少需要两个特性:原子性和可见性。
2.synchronize是具有原子性和可见性的,所以如果使用了synchronize修饰的操作,那么就自带了可见性,也就不再需要volatile来保证可见性了。
2.对于上面代码,若想实现线程安全的数字的自增自减等操作,也可使用 java.util.concurrent.atomic包来进行无锁的原子性操作。在其底层实现中,如AtomicInteger,同样是使用了volatile来保证可见性;使用Unsafe调用native本地方法CAS,CAS采用总线加锁或缓存加锁方式来保证原子性。
参考:
(Java并发编程:volatile关键字解析http://www.importnew.com/18126.html
《深入理解Java虚拟机:JVM高级特性与最佳实践》
0 个人打赏
文章最后发布于: 2017-10-08 04:18:47
展开阅读全文

没有更多推荐了,返回首页

©️2019 CSDN 皮肤主题: 数字20 设计师: CSDN官方博客

打赏

冉椿林博客

“你的鼓励将是我创作的最大动力”

5C币 10C币 20C币 50C币 100C币 200C币

分享到微信朋友圈

×

扫一扫,手机浏览