volatile的作用和实现原理

目录

作用

保证可见性

阻止指令重排序

什么是指令重排序

内存屏障

不是线程安全的


作用

  • 保证线程间的可见性
  • 防止指令重排序(有序性)
  • 依靠总线锁或者mesi协议,可以配合循环CAS构成乐观锁
  • 在原子类、AQS、单例模式有所应用

保证可见性

        被volatile修饰的变量在进行写操作的时候会做两件事:

  • 将当前处理器缓存行的数据写回到系统内存;
  • 使其他CPU里缓存了该内存地址的数据无效,其他地方要用到这个变量必须重新去内存读取。

        所以它能确保所有线程看到这个变量的值是一致的,具有可见性。相比synchronized加锁的方式来解决共享变量的内存可见性问题,volatile就是更轻量的选择,它没有上下文切换的额外开销成本。

阻止指令重排序

什么是指令重排序

        在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。重排序分3种类型:

  • 编译器优化的重排序。

        编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。

  • 指令级并行的重排序。

        现代处理器采用了指令级并行技术来将多条指令重叠执行(就是可以同时执行很多指令)。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。

  • 内存系统的重排序。

        由于处理器使用缓存和读/写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。

从Java源代码到最终实际执行的指令序列,会分别经历这3种重排序:

源代码 ——>> 编译器优化重排序 ——>> 指令级并行重排序 ——>> 内存系统的重排序 ——>> 最终执行的指令序列

举例:

        双重校验单例模式。Singleton instance=new Singleton();对应的JVM指令分为三步:分配内存空间-->初始化对象--->对象指向分配的内存空间。但是经过了编译器的指令重排序,第二步和第三步就可能会重排序。 (分配内存空间,向让对象指向分配的空间,再初始化对象)

        valatile通过分别限制编译器重排序和处理器重排序来保证有序性。

内存屏障

写操作:普通写——volatile写——读

  • 在每个volatile写操作的前面插入一个StoreStore屏障

        禁止上面的普通写操作和下面的volatile写重排序(写写屏障)

  • 在每个volatile写操作的后面插入一个StoreLoad屏障

        禁止上面的volatile写和下面可能的volatile读、写重排序(写读屏障)

读操作:volatile读——普通读写

  • 在每个volatile读操作的后面插入一个LoadLoad屏障

        禁止下面所有的普通读操作和上面的volatile读重排序(读读屏障)

  • 在每个volatile读操作的后面插入一个LoadStore屏障

        禁止下面所有的普通写操作和上面的volatile读重排序(读写屏障)

不是线程安全的

        volitile 可以保证有序性和可见性,但不能保证原子性,因此它不是线程安全的。

举例:

以 i++(非原子操作)为例:

    private volatile int i = 0,两个线程同时执行 i++,

        两个线程同时从主内存中拿到 i 的最新值 0 ,并且同时对 i 进行 +1 操作并将新值赋值回 i,最后同时将 +1 后的 i 值写回主内存中,最终 i == 1,很明显结果是错的。

你可能感兴趣的:(java,开发语言)