volatile的应用场景多是多线程,要保证多线程的正确有效操作,一般依靠3个方面:原子性、可见性、有序性,volatile而只能保证可见性与一定的有序性,因此其使用范围有所受限。volatile适用于以下情形:

  • 修改不依赖于自身状态的改变

  • 修改也不要在其他变量状态修改的条件中

第一种情形比较容易理解,因为volatile不支持原子操作,jvm支持的原子操作就是数值常量的赋值与变量的读取,甚至于i++这样的操作,jvm也无法保证其原子性,因为i++牵涉到3步操作,读取、修改、保存,所以,使用volatile修饰变量时,其修改操作不要依赖于自身状态。

第二种情形比较难于理解,举例说明一下:

public class TestVolatile{

        private volatile int upper;

        private volatile int lower;

        public void setUpper(int value) {

                if (value >= lower) {

                        upper = value;

                }

        }

        public void setLower(int value) {

                if(value <= upper) {

                        lower = value;

                }

        }

}

设置upper属性的时候,需要牵涉到lower的状态,可能会引起错误,某一时刻(10,3)分别为upper与lower的值,但是在多线程同时操作时,线程一调用setUpper(5), 线程二调用setLower(7),最终导致(5,7)这样错误的upper与lower的情形。

因此,使用volatile时务必遵守以上两条原则。

多线程并发操作的有效性保证依靠于上面提到的3个方面,原子性、可见性、有序性。

  • 原子性:执行某一动作的过程中的要么所涉及的操作同时执行,要么都不执行。volatile不支持原子性操作,所以,其保证并发性缺少一定的有效性;考虑使用synchronize或者lock。

  • 可见性:涉及到jvm的内存模型。在每个线程工作时,都会把主内存的内容拷贝一份到当前线程的工作内存中,然后操作当前线程的工作内存中的对象。jvm规范中说明,线程中的操作不可以直接涉及主内存的对象,而且各线程中的对象也是不可见的,更无法相互操作的。为了完成各线程中的对共享变量的可见性,需要使用锁操作或者使用缓存一致性协议。缓存一致性:当共享变量在某一线程中有更改操作,立即更新至主内存并向其他线程发出通知,该共享变量在工作内存的缓存无效,需要从主内存中重新加载最新的数值。

  • 有序性:编译器与处理器为了性能考虑会执行指令重排操作,导致有些操作顺序颠倒,但是执行结果是一致的,因此在单线程中,指令重排对程序的执行无任何影响,但对于多线程则不同,例如:

context=load();

boolean inited  = true;

doSomething(){

   if(inited){

        。。。。

    }

}

在load操作中完成对环境的初始化,然后调用doSomething方法,如果存在指令重排,多线程环境下,有可能还没有load操作,就已经设置inited为true,导致doSomething已经执行,这样与预期的执行效果不同。把inited变量使用volatile关键字修饰之后,编译器在编译的时候就会在inited变量前面加入lock指令,对于处理器来讲相当于添加了内存屏障(内存栅栏),从而保证指令不会重排序,执行有顺序,达到防止以上情况的出现的目的。对于volatile修饰的变量的如何添加的内存屏障呢?

volatile boolean inited;

int a = 1; // 操作1

int b = 2; // 操作2

inited = true; // 操作3

char c = 'c'; // 操作4

if(inited) { // 操作5

    double d = 1.2; // 操作6

}

编译之后,操作2与操作3之间添加一条StoreStore内存屏障指令,操作3与操作4之间添加一条StoreLoad内存屏障指令,操作5之后添加了LoadLoad内存屏障指令与LoadStore内存屏障指令

StoreStore指令:保证volatile写操作(操作3)执行前,操作1、操作2等普通读写执行已经完成

StoreLoad指令:保证volatile写操作(操作3)执行完成后,执行普通的读写指令

LoadLoad指令:保证volatile读操作(操作5)执行完成后,普通的读操作再执行

LoadStore指令:保证volatile读操作(操作5)执行完成后,普通的写操作(操作6)执行。

内存屏障的作用:

1)确保指令重排时,不会把后面的指令排到内存屏障之前,也不会把前面的指令放置到指令之后

2)强制对缓存的修改立即写入主存

3)如果是写操作,它会导致其他CPU中对应的缓存行失效


jvm在底层已经默认实现了有序性,即happens-before原则

1)指令的有序执行

2)unlock优先于lock

3)volatile写优先于volatile读

4)a优先于b,b优先于c,则a优先与c,即传递性

5)线程初始化优先于start

6)对线程interrupt方法的调用先行发生于被中断线程的检测中断事件发生

7)线程内所有操作优先于线程结束操作

8)对象的实例化优先于销毁

总结:volatile对于多线程操作有一定的作用,可以作为synchronize的辅助,毕竟锁操作的开销比较大,由于volatile只能保证一定的有序性与可见性,因此使用严格遵守其使用原则,则可以实现一定的并发操作。