volatile学习


一、主要作用

  1. 保证可见性
  2. 禁止指令重排

二、可见性

1. 是什么

可见性是由于多cpu而导致的缓存不一致问题
而JVM中用volatie来消除了它,可看图

2. 原理

volatile底层做了什么事情呢,其实就是volatile在赋值操作时,其后会执行一个指令,即内存屏障的功效,它有两个作用,其一是使得本cpu的高速缓存的该值会刷新到主内存,同时,使得其他cpu的高速缓存中的该值无效。这样便会在别的cpu使用时再去读取主内存的值,因此便做到了可见性,把缓存不一致性问题给消除掉了。其二是保证有依赖关系的变量操作
依赖关系代码示例:

// a = a * 2 依赖于 a = a + 10 
a = a + 10 ;
a = a * 2 ;
b = b + 10 

要将变量的值刷新会主内存时,必须保证之前所有的操作都已经执行完。
如a = a * 2 想刷新会主内存,在此之前必须保证先做了 a = a + 10 。

2.指令重排

2.1 是什么?

处理器会对代码进行指令重排以提升效率。
我们来看下什么是指令重排

参考代码
int a = 3 ;
a = a + 10 ;
// 它依赖于前面的操作,所以,重排不能够将它放在 a = a + 10之前进行
a = a * 2 ;
int b = 4 ;
int c = 5 ;

代码顺序是这样依次赋值,但是在实际中,可能会被cpu打乱顺序进行赋值。例如先执行了 int c = 5 ,当然这说的是没有依赖的代码指令之间的先后顺序。

2.2 指令重排在多线程存在问题

指令重排在一个线程当中是没问题,不会影响最终结果。
但是指令重排在多线程中就会出现问题,
指令重排的影响:

        Map configOptions;
        char[] configText;
        
        //此变量必须定义为volatile 
        volatile boolean initialized = false;
        
        //假设以下代码在线程A中执行 
        //模拟读取配置信息,当读取完成后将initialized设置为true以通知其他线程配置可用 
        configOptions = new HashMap();
        configText = readConfigFile(fileName);
        processConfigOptions(configText, configOptions);
        // 指令重排时,它可能会放在最开始执行,然后
        initialized = true;
        
        //假设以下代码在线程B中执行 
        //等待initialized为true,代表线程A已经把配置信息初始化完成 
        while (!initialized) {
            sleep();
        }
        
        //使用线程A中初始化好的配置信息 
        doSomethingWithConfig();

我们来看下指令重排可能会发生什么事:
指令重排是保证单线程中是正常的。


		// 指令重排时,它可能会放在最开始执行
        initialized = true;
		
		// 然后进行了配置准备
        configOptions = new HashMap();
        configText = readConfigFile(fileName);
        processConfigOptions(configText, configOptions);
        
        // 配置准备好后,进行一些业务处理
        doSomethingWithConfig();

单线程中,上面进行了指令重排是没问题的,但是放在多线程中,
它的优先执行,B线程的判断逻辑看到了,执行了对应的逻辑,可是,A线程其实没有准备好配置呢。

2.3 原理

在volatile的写操作即赋值时,会在指令把修改同步到内存时,保证之前所有的操作都已经完成,这样便形成了指令重排序无法越过内存屏障的效果。

3 使用示例

3.1 single = new 对象() 并非原子操作 && 双重锁校验 && 解决指令重排

下面我们将会通过双重锁校验来说明new对象的过程和可能出现指令重排的现象,并演示用volatile来解决:

3.1.1 代码准备

/**
 * 我们这里来分析下为什么双重锁校验会需要volatile
 */
public class 单例模式的应用_双重锁校验 {

    private static volatile 单例模式的应用_双重锁校验 single ;

    private 单例模式的应用_双重锁校验(){}

    public static 单例模式的应用_双重锁校验 getSingle(){
        if(single == null){
            synchronized (单例模式的应用_双重锁校验.class){
                if (single == null){
                    single = new 单例模式的应用_双重锁校验();
                }
            }
        }
        return single;
    }
}

3.1.2 获取字节码指令 && 分析

通过这个命令可以获取到JVM字节码指令集:

javap -c 单例模式的应用_双重锁校验.class > 单例模式的应用_双重锁校验.txt

得到:

Compiled from "单例模式的应用.java"
public class com.example.demo.primary.volatile学习.单例模式的应用 {
  public com.example.demo.primary.volatile学习.单例模式的应用 getSingle();
    Code:
       0: getstatic     #2                  // Field single:Lcom/example/demo/primary/volatile学习/单例模式的应用;
       3: ifnonnull     37
       6: ldc           #3                  // class com/example/demo/primary/volatile学习/单例模式的应用
       8: dup
       9: astore_1

	 // 我们着重关注锁住的这段代码
      10: monitorenter
      11: getstatic     #2                  // Field single:Lcom/example/demo/primary/volatile学习/单例模式的应用;
      14: ifnonnull     27
		
	 // 以下就是new对象时的指令集	
      17: new           #3                  // class com/example/demo/primary/volatile学习/单例模式的应用
      20: dup
      21: invokespecial #4                  // Method "<init>":()V
      24: putstatic     #2                  // Field single:Lcom/example/demo/primary/volatile学习/单例模式的应用;
      27: aload_1
      28: monitorexit



      29: goto          37
      32: astore_2
      33: aload_1
      34: monitorexit
      35: aload_2
      36: athrow
      37: getstatic     #2                  // Field single:Lcom/example/demo/primary/volatile学习/单例模式的应用;
      40: areturn
    Exception table:
       from    to  target type
          11    29    32   any
          32    35    32   any
}

我们着重关注下创建对象的指令集:

      // 1.创建对象,分配内存	
      17: new           #3                  
      
      // 2.操作数栈准备
      20: dup
      
      // 3.实例初始化,即<init>执行
      21: invokespecial #4          // Method "<init>":()V

	  // 4.将对象赋值给引用变量
      24: putstatic     #2     
      
      // 5. 压入栈顶             
      27: aload_1

以上我们着重来看1,3,4三个步骤,由于3,和 4 都依赖于1,因此指令重排时必定在其后,而3,4没有依赖关系,可能有顺序变动。

线程一执行:

public class 单例模式的应用_双重锁校验 {

    private static volatile 单例模式的应用_双重锁校验 single ;

    private 单例模式的应用_双重锁校验(){}

    public static 单例模式的应用_双重锁校验 getSingle(){
        if(single == null){
            synchronized (单例模式的应用_双重锁校验.class){
                if (single == null){
                    // 假设当前线程一执行到了这里,由于这里对应多个指令
                    // 假设指令重排后的顺序时1,4,3
                    single = new 单例模式的应用_双重锁校验();
                }
            }
        }
        return single;
    }
}

假设当线程一执行了1,4后,即先将创建出来的对象进行了赋值给引用变量,由于这个引用变量是共享资源,操作系统的cpu时间片假设在此时分给了线程二。而线程二拿了single判断不为null后要进行使用,由于线程一已经将对象赋值给了single,因此,线程二就会拿到还没有初始化的single,因此,就会出现问题。

3.2 volatile 不能做到线程安全

提示:volatile是可以做到指令重排和缓存一致的可见性。但是依然不是线程安全的,我们通过一个 i++ 来演示。

3.2.1 代码准备 && 分析

/**
 * volatile变量自增运算测试 **@author zzm
 */
public class Volatile是线程不安全的 {

    public static volatile int race = 0;

    public static void increase() {
        race++;
        System.out.println(race+"++");
    }

    private static final int THREADS_COUNT = 20;

    public static void main(String[] args) {
        System.out.println("----------->");
        for (int i = 0; i < THREADS_COUNT; i++) {
            new Thread(
                    ()->{
                        for (int j = 0; j < 10000; j++) {
                            increase();
                        }
                    }
            ).start();
        }
        //等待所有累加线程都结束
        int count = 0 ;
        while ( (count = Thread.activeCount()) > 2 ){
            System.out.println("当前活跃线程数:"+count);
            Thread.yield();
        }
        System.out.println(race);
    }
}

执行结果总会小于200000,因为虽然volatile可以保证缓存一致,但是各cpu同一时刻从自己的工作内存中拿到 race ,假设都是100,因为 i ++ 不是原子操作,当从工作区拿到栈顶时,都是100,肯定有的线程先执行完,例如是101,然后刷新了其他工作内存,但是,由于其他cpu的栈是线程私有的,且之前已经读取过了值,为100,压栈成功了,因此,此刻其他cpu执行完后还是101,并刷新了主内存,这样就导致丢失更新,导致最终结果小于20 0000 。而如果将 读取和 加一整体变为原子操作,便可避免这个问题。


总结

提示:volatile的性能和优化过后的synchronized没有差别,因此,这个不是考虑选谁的一个因素。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值