volatile图文详解
一、定义-是什么volatile 是 Java 提供的轻量级同步机制非锁机制仅能修饰「成员变量 / 静态成员变量」核心解决多线程下共享变量的可见性和有序性问题但不保证原子性。volatile 修饰的变量会保证线程对该变量的修改会立即刷新到主内存线程对该变量的读取会先失效工作内存中的副本直接从主内存加载最新值禁止编译器和处理器对该变量相关的指令进行重排序。在Java代码中volatile关键字主要有两层语义• 不同线程对volatile变量的值具有内存可见性即一个线程修改了某个volatile变量的值该值对其他线程立即可见。• 禁止进行指令重排序还能确保执行的有序性。volatile语义中的有序性是通过内存屏障指令来确保的。而volatile关键字可以禁止代码重排序比如有如下代码那么这会有4种情况发生1AB可以重排序2CD可以重排序3AB不可以重排到Z的后面4CD不可以重排到Z的前面。也就是说变量Z是一道屏障是一堵墙Z变量之前或之后的代码不可以跨越Z变量。synchronized关键字也具有同样的特性。补充了解每个Java线程都拥有一个独立的工作内存同时有个全局内存堆内存来存储数据。当线程需要访问一个变量时首先将其复制到线程的工作内存中。之后线程每次对该变量的操作都将是对线程栈中的副本进行操作的。如果变量是被volatile修饰的每次变量都会直接从内存中读取数据每次对变量修改都会实时同步到内存中这样就能确保变量的多线程实时可见。volatile对任意变量的读写都具备原子性但对复合操作不具备原子性。所有基础类型与引用类型的赋值都是原子性的但JVM会将i这类复合操作解析成多条指令来执行所以不是原子操作。把内存分为「主内存」所有线程共享和「工作内存」每个线程私有存储变量副本普通变量线程修改的是工作内存副本刷新到主内存的时机不确定 → 其他线程看不到最新值可见性问题volatile 变量修改后「强制刷回主内存」读取前「强制从主内存加载」→ 所有线程看到的是同一个值同时禁止指令乱序执行 → 避免逻辑错误有序性问题。在32位JDK中针对未使用volatile声明的long或double的64位数据类型没有实现写原子性如果想实现需要在声明变量时添加volatile而在64位JDK中是否具有原子性取决于具体的实现在X86架构64位JDK版本中写double或long是原子性的。针对用volatile声明的int i变量进行i操作时是非原子性的。表达式i的操作步骤分解如下1从内存中取出i的值2计算i的值3将i的值写到内存中。二、使用场景使用原则遵守以下原则才能安全使用volatile避免线程安全问题原则 1变量的写入操作不依赖当前值如果修改变量需要用到变量自身的旧值如累加、自增volatile无效因为需要原子性。原则 2只有单个线程修改变量多线程仅读取多线程同时写入volatile变量会因为原子性问题导致数据错乱。原则 3变量不需要与其他状态变量构成不变约束比如a b、a b 10这类关联约束与变量b有关系volatile无法保证整体一致性。原则 4用于简单的状态标记 / 一次性发布场景替代重量级锁追求高性能的简单同步场景。不需要保证原子性仅需要保证可见性和有序性。高频场景1. 状态标记位最常用用于标记线程是否停止、任务是否完成等仅需单一读 / 写操作无需原子性。2. 双重校验锁DCL单例模式如前文示例volatile 禁止instance new Singleton()的指令重排避免获取到未初始化的对象。3. 多线程读写单一变量无复合操作用于单线程更新、多线程读取的实时状态值如配置开关、系统温度、心跳状态无i、i - 1等复合操作。4. 防止指令重排如初始化依赖变量仅初始化一次后续所有线程只读volatile保证初始化后对所有线程可见。volatile保证对象初始化完成后其他线程能看到最新的对象状态适用于初始化后只读的场景。不适用场景使用注意事项复合操作如i、i1等包含读取、计算、写入三步的操作多变量协同变更多个volatile变量之间存在逻辑依赖关系临界区互斥需要防止两个线程同时进入初始化块等场景高频写操作在写操作竞争激烈时volatile的性能优势会降低引用类型限制volatile只保证引用本身的可见性不保证其指向对象内部状态的可见性如volatile List listlist引用变化可见但list.add()仍需同步不能替代锁volatile无法替代synchronized或Lock当需要原子性时必须使用锁或原子类性能考量volatile写操作因涉及内存屏障和缓存同步开销略高于普通变量但远低于synchronized三、底层实现原理内存屏障内存屏障分为写屏障(Store Barrier)与读屏障(Load Barrier)。写屏障会强制刷新存储缓存会将存储缓存中的数据同步到其他CPU缓存中。读屏障会强制CPU立刻处理失效队列中的所有消息消息队列中所有要失效的数据会被依次失效。volatile 的可见性和有序性本质是通过内存屏障Memory Barrier实现的JVM 会为 volatile 变量添加以下 4 类内存屏障volatile变量的内存语义写 volatile 变量执行「StoreStore 屏障」保证当前写操作之前的所有普通变量写操作都刷新到主内存执行「StoreLoad 屏障」保证当前 volatile 写操作刷新到主内存后才执行后续操作。读 volatile 变量执行「LoadLoad 屏障」保证当前读操作之后的所有读操作都从主内存加载执行「LoadStore 屏障」保证当前 volatile 读操作加载完成后才执行后续写操作。JMM建议JVM采取保守策略对重排序进行严格禁止。下面是基于保守策略的volatile操作的内存屏障插入策略。• 在每个volatile读操作的后面插入一个LoadLoad屏障。• 在每个volatile读操作的后面插入一个LoadStore屏障。• 在每个volatile写操作的前面插入一个StoreStore屏障。• 在每个volatile写操作的后面插入一个StoreLoad屏障。volatile写操作的内存屏障插入策略为在每个volatile写操作前插入StoreStore(SS)屏障在写操作后面插入StoreLoad屏障具体如图4-15所示。volatile读操作的内存屏障插入策略为在每个volatile写操作后插入LoadLoad(LL)屏障和LoadStore屏障禁止后面的普通读、普通写和前面的volatile读操作发生重排序具体如图4-16所示。上述JMM建议的volatile写和volatile读的内存屏障插入策略是针对任意处理器平台的所以非常保守。不同的处理器有不同“松紧度”的处理器内存模型只要不改变volatile读写操作的内存语义不同JVM编译器可以根据具体情况省略不必要的JMM屏障。五、了解重排序为了提高性能编译器和CPU常常会对指令进行重排序。重排序主要分为两类编译器重排序和CPU重排序1 编译器重排序编译器重排序指的是在代码编译阶段进行指令重排不改变程序执行结果的情况下为了提升效率编译器对指令进行乱序(Out-of-Order)的编译。2 CPU重排序流水线(Pipeline)和乱序执行(Out-of-Order Execution)是现代CPU基本都具有的特性。机器指令在流水线中经历取指令、译码、执行、访存、写回等操作。为了CPU的执行效率流水线都是并行处理的在不影响语义的情况下。处理次序Process Ordering机器指令在CPU实际执行时的顺序和程序次序Program Ordering程序代码的逻辑执行顺序是允许不一致的只要满足As-if-Serial规则即可。显然这里的不影响语义依旧只能保证指令间的显式因果关系无法保证隐式因果关系即无法保证语义上不相关但是在程序逻辑上相关的操作序列按序执行。As-if-Serial规则的具体内容为无论如何重排序都必须保证代码在单线程下运行正确。As-if-Serial规则只能保障单内核指令重排序之后的执行结果正确不能保障多内核以及跨CPU指令重排序之后的执行结果正确。