线程安全与volatile关键字

volatile关键字,语义有二:html

  1. volatile修饰的变量对于其余线程具备当即可见性
  2. 禁止指令重排序

下面进行详细介绍,并聊聊Java先行发生原则与volatile。java

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+一、将结果同步回主内存。那么就有可能一个线程拿到值,正开始执行i+1,而值还将来得及改变时,另外一个线程也一样正在进行i+1。这样一来,就有可能两个线程给同一个值加了一次1,因此就算有volatile修饰也是无力回天。安全

这时,咱们应该使用synchronize或concurrent原子类来保证“操做”的原子性。固然“一写多读”是线程安全的,由于不涉及到多个线程来“写”,致使的值重复写入问题。故volatile的使用场景应该是:修饰的变量的有关操做都是原子性的时候。好比修饰一个控制标志位:并发

volatile boolean tag=true;

线程1 while(tag){};

线程2 while(tag){};
复制代码

当tag=false时,两个线程都能立刻感知到并中止while循环,由于简单的赋值语句属于原子操做(请注意是:赋予具体的值而不是变量),它只负责把主内存的tag同步为true。this

能实现可见性的关键字除了volatile,还有synchronize与final:atom

  • synchronize是由于变量执行解锁操做前,会把变量同步到主内存(自带可见性);spa

  • final则是被其修饰的变量一旦初始化,且构造器没有把this引用传递到外面去的状况下,其余线程就能够看见它的值(由于它永不发生变化)。线程

禁止指令重排序

new一个对象能够分解为以下的3行伪代码

memory=allocate(); //1:分配对象的内存空间

ctorInstance(memory); //2:初始化对象

instance=memory; //3:设置instance指向刚分配的内存地址
复制代码

上面三行代码中的2和3之间,可能会被重排序(在一些JIT编译器上,这种重排序是真实发生的)。2和3之间重排序以后的执行顺序可能以下:

memory=allocate(); //1:分配对象的内存空间

instance=memory; //3:设置instance指向刚分配的内存地址,注意此时对象尚未被初始化

ctorInstance(memory); //2:初始化对象
复制代码

若是发生重排序,另外一个并发执行的线程B就有可能在还没初始化对象操做前就拿走了instance,但此时这个对象可能尚未被线程真正初始化,所以这是线程不安全的。

“Java先行发生原则”与valatile

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来保证可见性了。

  3. 若想实现线程安全的数字的自增自减等操做,也可以使用java.util.concurrent.atomic包来进行无锁的原子性操做。在其底层实现中,如AtomicInteger,一样是:

    • 使用了volatile来保证可见性

    • 使用Unsafe调用native本地方法CAS,CAS采用总线加锁或缓存加锁方式来保证原子性。

参考:

(Java并发编程:volatile关键字解析)www.importnew.com/18126.html

《深刻理解Java虚拟机:JVM高级特性与最佳实践》

相关文章
相关标签/搜索