Java的volatile

volatile作用对象:volatile只作用于共享变量。

共享变量:在多个线程之间能够被共享的变量被称为共享变量。共享变量包括所有的实例变量,静态变量和数组元素。

 

volatile作用

1、同步(可见性)

同synchronized相比(synchronized通常称为重量级锁),volatile更轻量级,因为它不会引起线程上下文的切换和调度。

如图,如果变量没有volatile关键字,那么A线程对该变量的改变存储在内存A,B变量不可知。

将一个共享变量声明为volatile后,会有以下效应:

    1.当写一个volatile变量时,JMM会把该线程对应的本地内存(也称作“工作内存”)中的变量强制刷新到主内存中去;

    2.这个写操作会导致其他线程中的缓存无效。

本地内存是线程所在CPU的Cache:

 

volatile关键字不仅可以保证变量直接从主内存中读取,还有以下作用:

  • 当一个线程对一个volatile变量进行写操作的时候,不仅仅是这个变量自己被写入到主存中,同时,其他所有在这之前被改变值的变量也都会线程先写入到主存中。
  • 当一个线程对一个volatile变量进行读取操作,他也会将所有跟着那个volatile变量一起写入到主存中的其他所有变量一起读出来。
Thread A:
    sharedObject.nonVolatile = 123;
    sharedObject.counter     = sharedObject.counter + 1;

Thread B:
    int counter     = sharedObject.counter;
    int nonVolatile = sharedObject.nonVolatile;

这个例子中,nonVolatile和counter一样,也会被写入主存,以及从主存中读取出来。

2、禁止指令重排序优化

操作volatile变量的读写指令的顺序无法被JVM改变,即其语句本身的指令不会重排,且其之前的语句,和之后的语句不会互串。

重排序在多线程中可能存在的问题:

public class TestVolatile {
    int a = 1;
    boolean status = false;

    /**
     * 状态切换为true
     */
    public void changeStatus(){
        a = 2;//1
        status = true;//2
    }

    /**
     * 若状态为true,则running。
     */
    public void run(){
        if(status){//3
            int b = a+1;//4
            System.out.println(b);
        }
    }
}

上述第1、2步由于不存在依赖关系,可能会被系统重排序,从而不能保证第四步的b=3。

 

volatile不足之处:

非原子操作出现的问题:

package test;

import java.util.concurrent.CountDownLatch;

/**
 * Created by chengxiao on 2017/3/18.
 */
public class Counter {
    public static volatile int num = 0;
    //使用CountDownLatch来等待计算线程执行完
    static CountDownLatch countDownLatch = new CountDownLatch(30);
    public static void main(String []args) throws InterruptedException {
        //开启30个线程进行累加操作
        for(int i=0;i<30;i++){
            new Thread(){
                public void run(){
                    for(int j=0;j<10000;j++){
                        num++;//自加操作
                    }
                    countDownLatch.countDown();
                }
            }.start();
        }
        //等待计算线程执行完
        countDownLatch.await();
        System.out.println(num);
    }
}

得到的结果并不是300000,而是224291,原因是num++不是个原子性的操作,而是个复合操作(读取、加1、赋值)。所以,在多线程环境下,有可能线程A将num读取到本地内存中,此时其他线程可能已经将num增大了很多,线程A依然对过期的num进行自加,重新写到主存中。

解决方法:通过使用Automic原子操作类

/**
 * Created by chengxiao on 2017/3/18.
 */
public class Counter {
  //使用原子操作类
    public static AtomicInteger num = new AtomicInteger(0);
    //使用CountDownLatch来等待计算线程执行完
    static CountDownLatch countDownLatch = new CountDownLatch(30);
    public static void main(String []args) throws InterruptedException {
        //开启30个线程进行累加操作
        for(int i=0;i<30;i++){
            new Thread(){
                public void run(){
                    for(int j=0;j<10000;j++){
                        num.incrementAndGet();//原子性的num++,通过循环CAS方式
                    }
                    countDownLatch.countDown();
                }
            }.start();
        }
        //等待计算线程执行完
        countDownLatch.await();
        System.out.println(num);
    }
}

 

 volatile使用场景:

必须同时满足下面两个条件:

  • 对于非原子操作,对变量的写操作不依赖于当前值。
  • 该变量没有包含在具有其他变量的不变式中。

Eg1: 状态标志。使用一个布尔状态标志,用于指示发生了一个重要的一次性事件,例如,shutdownRequested 标志从false 转换为true,然后程序停止。这种模式可以扩展到来回转换的状态标志,但是只有在转换周期无需被察觉的情况下才能扩展。

Eg2: 一次性安全发布(one-time safe publication)。例如单例模式,这里volatile的作用仅仅是阻止指令重排序, 不涉及可见性问题, 可见性已经由synchronized来保证了。

private volatile static Singleton instance;     
    
public static Singleton getInstance(){     
    //第一次null检查       
    if(instance == null){              
        synchronized(Singleton.class) {    //1       
            //第二次null检查         
            if(instance == null){          //2    
                instance = new Singleton();//3    
            }    
        }             
    }    
    return instance;   
}

初始化对象的指令正常顺序:

(1)分配内存空间。 

(2)初始化对象。

(3)将内存空间的地址赋值给对应的引用。  

但(2)、(3)是可能交换的,交换会导致的问题:在发生重排序的情况下,会导致线程B在 t3 时间下,判断出 singleton 不为null。

时间线程A线程B
t1 分配对象空间  
t2 设置 instance 指向分配的内存空间  
t3   判断 instance 是否为空
t4   由于 instance 不为null,线程B将访问instance 引用的对象
t5 初始化对象  
t6 访问 instance 引用的对象

 

Eg3: 独立观察(independent observation)。定期 “发布” 观察结果供程序内部其他线程读取这个变量。

Eg4:开销较低的“读-写锁”策略。如果读操作远远超过写操作,您可以结合使用内部锁和 volatile 变量来减少公共代码路径的开销。

public class CheesyCounter {  
    // Employs the cheap read-write lock trick  
    // All mutative operations MUST be done with the 'this' lock held  
    @GuardedBy("this") private volatile int value;  
  
    //读操作,没有synchronized,提高性能  
    public int getValue() {   
        return value;   
    }   
  
    //写操作,必须synchronized。因为x++不是原子操作  
    public synchronized int increment() {  
        return value++;  
    }  
}

 

volatile性能:
读写一个volatile变量的时候,会导致变量直接在主存中读写,显然,直接从主存中读写速度要比从cache中来得慢。另一方面,操作volatile变量的时候不能改变指令的执行顺序,这一定程度上也会影响读写的效率。

posted on 2019-05-17 17:17  joannae  阅读(199)  评论(0编辑  收藏  举报

导航