探讨 volatile 关键字

目录

引入:

1、volatile含义

2、volatile三个特性:

(1、保证可见性

Java的内存模型JMM(Java Memory Model)

(2、不保证原子性

(3、禁止指令重排序

单例模式的双重锁中要加volatile


引入:

为什么使用 volatile关键字呢?给大家举个例子吧!!!

很大的一个原因就是关于编译器自动优化的问题,看下面一段代码:

class Counter{
    public static int count;
}
public class Test {
    public static void main(String[] args) {

        Thread t1 = new Thread(()-> {
            while(Counter.count==0) {
            
            }
            System.out.println("t1线程结束");
        });
        Thread t2 = new Thread(()-> {
            System.out.println("输入一个数:");
            Scanner scanner = new Scanner(System.in);
            Counter.count = scanner.nextInt();
            
        });
        t1.start();
        t2.start();
    }
}

         以上代码,当你执行时,你会发现你输入一个数,改变了count的值,但代码依旧没停止,还在while循环中执行着,出现这个问题的原因就是编译器的自动优化,到底是怎么优化的?

        首先每进行while条件的比较时,都会读取内存中count的值,将其加载到CPU寄存器中,再进行计算比较处理。而在此时,编译器会以为没有人再去修改count的值,而读取内存加载到寄存器这又是相对比较大的开销(与计算比较处理相比),所以编译器在这里自动优化,省去了读取内存这一步骤,直接取寄存器的数据进行比较。

        这里需要注意的是,编译器自动优化这件事,对于我们这些没有编写过JVM的程序员来说,算是一个未知的东西,例如上述代码我们在循环里面加上代码:

                try {
                    Thread.sleep(500);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }

我们再次运行时,编译器又没有对其进行优化了 

1、volatile含义

        volatile是Java提供的一种轻量级的同步机制。Java 语言包含两种内在的同步机制:同步块(或方法)和 volatile 变量,相比于synchronized(synchronized通常称为重量级锁),volatile更轻量级,因为它不会引起线程上下文的切换和调度。但是volatile 变量的同步性较差(有时它更简单并且开销更低),而且其使用也更容易出错。

        volatile可以保证线程可见性且提供了一定的有序性,但是无法保证原子性。在JVM底层volatile是采用“内存屏障”来实现的。观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令,lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:

  • 它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
  • 它会强制将对缓存的修改操作立即写入主存;
  • 如果是写操作,它会导致其他CPU中对应的缓存行无效。
     

2、volatile三个特性:

 关于这三个特性是什么意思,这篇博客里面有说明:http://t.csdn.cn/LN8qP 

(1、保证可见性

//保证内存可见性
class Counter{
    public static volatile int count;
}
public class Test1 {
    public static void main(String[] args) {

        Thread t1 = new Thread(() -> {
            while (Counter.count == 0) {

            }
            System.out.println("t1线程结束");
        });
        Thread t2 = new Thread(() -> {
            System.out.println("输入一个数:");
            Scanner scanner = new Scanner(System.in);
            Counter.count = scanner.nextInt();

        });
        t1.start();
        t2.start();
    }
}

加了关键字之后,输入一个值改变coun值后,循环结束

Java的内存模型JMM(Java Memory Model)

        volatile禁止了编译器优化,避免了直接读取CPU寄存器中缓存的数据,而是每次都重新读内存。

        用Java的术语来说,应该是:站在JMM的角度看待volatile,正常的程序执行过程中,会把主内存的数据,先加载到工作内存中,再进行计算处理,编译器优化可能会导致不是每次都真的去读取主内存,而直接读取工作内存中的缓存数据(可能导致内存可见性问题),而volatile起到的效果,就是保证每次读取内存都是真的从主存中重新读取

(2、不保证原子性

//不保证原子性
public class Test2 {
    public static volatile int count;

    public static void main(String[] args) {
        Thread t1 = new Thread(() -> {
            while(count<100) {
                System.out.println(count++);
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        Thread t2 =  new Thread(() -> {
            while(count<100) {
                System.out.println(count++);
                try {
                    Thread.sleep(100);
                } catch (InterruptedException e) {
                    throw new RuntimeException(e);
                }
            }
        });
        t1.start();
        t2.start();
    }
}

这里依旧会出现两个线程输出的count的值相同

(3、禁止指令重排序

以单例模式举例:

单例模式的双重锁中要加volatile

public class Singleton {
    private volatile static Singleton instance;
    private Singleton (){}
    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

 代码分析:

         双锁模式,进行了两次的判断,第一次是判断是否要加锁,第二次是判断是否要创建实例。由于singleton=new Singleton()对象的创建在JVM中可能会进行重排序,例如:new对象时,大致可以分为三个步骤:

  • 申请内存,得到内存首地址
  • 调用构造方法,初始化实例
  • 把内存的首地址赋值给instance引用

        此时可能会出现编译器自动优化,将步骤2和3调换顺序,而刚好在步骤1、3执行完,步骤2未执行时,另一个线程调用getInstance,这时会认为instance非空,直接返回instance,并且在后续可能会针对instance进行解引用操作,而解决这样的问题,办法就是禁止指令重排 ,使用volatile修饰signleton实例变量有效,解决该问题。

下期见!!!

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

龙洋静

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值