美文网首页
java-volatile解析

java-volatile解析

作者: 天命_6236 | 来源:发表于2020-09-01 14:26 被阅读0次

    volatile关键字,熟悉的朋友应该能总结到该关键字的特征如下:

    1.保证可见性、但不保证原子性

    2.禁止指令重排

    在这里有一些理论的概念需要理解清楚,什么是可见性、原子性?

    这一部分的内容其实涉及到Java内存模型(JMM),什么是JMM?本质上可以理解为JVM规范了如何提供按需禁用缓存和编译优化的方法。

    在这之前我们知道并发编程bug的源头有:CPU缓存带来的可见性问题、线程切换带来的原子性问题和编译优化带来的有序性问题。

    背景:随着科技的不断发展,我们的cpu、内存、I/O设备都在不断迭代,变得越来越快,但是有一个核心的矛盾存在,就是这三者的速度差异。从速度上来说,cpu > 内存 > I/O,但是根据木桶原理(一只水桶能装多少水是取决于最短的那块木板),程序整体的性能是取决于最慢的操作-比如读写I/O设备。为了合理利用CPU的高性能,平衡这三者的速度差异,计算机体系机构、操作系统、编译程序都作出了贡献,主要表现为:

    • CPU增加了缓存,以均衡与内存的速度差异
    • 操作系统增加了进程、线程,以分时复用CPU,进而均衡了CPU与I/O设备的速度差异
    • 编译程序优化指令执行次序,使得缓存能够得到更加合理的利用

    可见性:一个线程对共享变量的修改,另外一个线程能够立刻看到,称之为可见性。

    image

    原子性:可以理解为一个操作要么全部完成,否则全部失败。

    举个案例:代码中常见的cont + 1操作,其实实现起来分为三部,

    指令 1:首先,需要把变量 count 从内存加载到 CPU 的寄存器;

    指令 2:之后,在寄存器中执行 +1 操作;

    指令 3:最后,将结果写入内存(缓存机制导致可能写入的是 CPU 缓存而不是内存)。

    在多线程的情况下,线程A可能执行了步骤1、cpu时间片切换到了线程B开始执行,导致count=1写入到内存中,线程A再此执行count+1=1,将线程内缓存写入到主内存中,结果被覆盖了

    image

    有序性:编译器了优化性能,有时候会改变程序中语句的先后顺序。

    例如程序中”a = 6;b =7”,可能会变成”b = 7;a =6” 这样可能会导致意想不到的BUG。

    一个经典的java案例就是利用双重检查创建单例对象:

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

    在多线程调用时会存在问题,问题出在new操作上。

    • a. 给singleton分配内存
    • b. 在内存中初始化singleton对象
    • c. 将内存地址赋给singleton变量
    image

    目前多采用静态内部类无锁的单例模式

    public class Singleton {
        private Singleton(){};
     
        static class innerHolder{
            private  static  Singleton instance = new Singleton();
        }
        public static Singleton getInstance(){
            return innerHolder.instance;
        }
    }
    

    特点:静态内部类实现单例模式,既能保证延迟加载,又能保证线程安全,只创建一个实例对象。

    原理解析:Singleton类初始化的时候不会加载所有的类,只有第一次访问静态内部类的时候才会进行初始化操作。

    虚拟机会保证一个类的 clinit() 方法在多线程环境中被正确的加锁、同步,如果多个线程同事去初始化一个类,那么只有一个线程去执行这个类的 clinit() 方法,其他线程都需要阻塞等待,直到活动线程执行 clinit() 方法执行完毕。具体可以参考《JVM-类加载机制》,这里不再深入。

    相关文章

      网友评论

          本文标题:java-volatile解析

          本文链接:https://www.haomeiwen.com/subject/ltdvsktx.html