volatile关键字

  • volatile保证可见性
  • volatile不保证原子性
  • volatile保证有序性

1.volatile保证可见性

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:
1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即
可见的。
2)禁止进行指令重排序
先看一段代码,假如线程1先执行,线程2后执行:
//线程1
boolean stop = false;
while(!stop){
    doSomething();
}
//线程2
stop = true;
这段代码是很典型的一段代码,很多人在中断线程时可能都会采用这种标记办法。但是事实上,这段代码会完全运行
正确么?即一定会将线程中断么?不一定,也许在大多数时候,这个代码能够把线程中断,但是也有可能会导致无法
中断线程(虽然这个可能性很小,但是只要一旦发生这种情况就会造成死循环了)。
但是用volatile修饰之后就变得不一样了:

2.volatile不能确保原子性

public class Test {
    public volatile int inc = 0;
    public void increase() {
        inc++;
    }
    public static void main(String[] args) {
        final Test test = new Test();
        for(int i=0;i<10;i++){
            new Thread(){
                public void run() {
                    for(int j=0;j<1000;j++)
                     test.increase();
                };
            }.start();
        }
        while(Thread.activeCount()>1)  //保证前面的线程都执行完
            Thread.yield();
        System.out.println(test.inc);
    }
}

大家想一下这段程序的输出结果是多少?也许有些朋友认为是10000。但是事实上运行它会发现每次运行结果都不一
致,都是一个小于10000的数字。
根源就在自增操作不是原子性操作,而且volatile也无法保证对变量的任何操作都是原子性的。
解决方案:可以通过synchronized或lock,进行加锁,来保证操作的原子性。也可以通过AtomicInteger。
在java 1.5的java.util.concurrent.atomic包下提供了一些原子操作类,即对基本数据类型的 自增
(加1操作),自减(减1操作)、以及加法操作(加一个数),减法操作(减一个数)进行了封装,保证
这些操作是原子性操作。atomic是利用CAS来实现原子性操作的(Compare And Swap),CAS实际上是利用
处理器提供的CMPXCHG指令实现的,而处理器执行CMPXCHG指令是一个原子性操作。

3.volatile保证有序性

//线程1:
context = loadContext();   //语句1
inited = true;             //语句2

//线程2:
while(!inited ){
    sleep()
}
doSomethingwithconfig(context);
前面举这个例子的时候,提到有可能语句2会在语句1之前执行,那么久可能导致context还没被初始化,而线程2中就使用未初始化的context去进行操作,导致程序出错。
这里如果用volatile关键字对inited变量进行修饰,就不会出现这种问题了,因为当执行到语句2时,必定能保证context已经初始化完毕。

例如利用双重检查锁定来创建单例
public static Singleton getInstance(){  
    if(instance == null){  
        synchronized(Singleton.class){  
            if(instance==null)  
                instance = new Singleton();  
            }  
        }  
    return instance;  
}
问题其实出在java的编译器上,java的编译器会将字节码命令进行重排序以便进行优化,在第五行,构造函数的
调用似乎应该在instance得到赋值之前发生,但是在java虚拟机内部,却不是这样的,完全有可能先new出来
一个空的未调用过构造函数的instance对象,然后再将其赋值给instance引用,然后再调用构造函数,对
instance对象当中的元素进行初始化。    
这样,就很有可能,当instance被赋值一个空的实例对象的时候,另一个线程调用了getInstance()这个函数,
另一个线程发现,instance并不是空的,于是愉快地return回了那个空的instance对象。 
可以通过写上volatile修饰instance防止指令重排序。
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容