volatile详解

volatile概述

volatile关键字Java 虚拟机提供最轻量级同步机制,它作为一个修饰符,用来修饰变量。它保证变量对所有线程可见性,禁止指令重排,但不保证原子性
(1)多线程主要围绕可见性和原子性两个特性而展开,使用volatile关键字修饰的变量,保证了其在多线程之间的可见性,即每次读取到volatile变量,一定是最新的数据
(2)使用volatile则会对禁止语义重排序,当然这也一定程度上降低了代码执行效率
可见性
不保证原子性
禁止指令重排(有序性)设置内存屏障来禁止指令重排

volatile详解

在 Java 中,volatile 是一种关键字,用于声明变量。当一个变量被 volatile 修饰时,表示它是易变的(即可能被多个线程同时修改),并且要求线程在读取该变量的值时直接从内存中进行读取,而不是从线程的工作内存中读取。这样可以确保对该变量的读写操作都是原子的,并且能够及时感知到其他线程对该变量的修改。
使用 volatile 关键字修饰的变量具有以下特性:

  1. 可见性:当一个线程修改了 volatile 变量的值,其他线程能够立即看到最新的值,不会出现数据脏读的情况。
  2. 禁止指令重排序:对 volatile 变量的写入操作前面的所有操作都发生在该写入操作之前,读取操作后面的所有操作都发生在该读取操作之后,这样可以防止指令重排序导致的问题。
    需要注意的是,虽然 volatile 能够提供可见性和禁止指令重排序的特性,但它并不能保证原子性。如果一个操作依赖于当前值,并且对该值的修改不是原子的,那么就需要额外考虑同步机制来确保原子性。
    通常情况下,volatile 适合于以下场景:
    ● 在多线程环境下,某个变量的值会被多个线程共享,并且这些线程可能会对该变量进行修改。
    ● 对该变量的修改并不依赖于当前值,或者能够通过额外的同步机制来确保原子性操作。
    总之,volatile 是 Java 中用于保证可见性和禁止指令重排序的关键字,能够帮助开发者在多线程环境下正确地处理共享变量的访问。
    可以保证在多线程环境下共享变量的可见性。通过增加内存屏障防止多个指令之间的重排序,我理解的可见性,是指当某一个线程对共享变量的修改,其他线程可以立刻看到修改之后的值
    CPU 层面的高速缓存,在 CPU 里面设计了三级缓存去解决 CPU 运算效率和内存 IO 效率问题,但是带来的就是缓存的一致性问题,而在多线程并行执行的情况下,缓存一致性就会导致可见性问题
    所以,对于增加了 volatile 关键字修饰的共享变量,JVM 虚拟机会自动增加一个#Lock 汇编指令,这个指令会根据 CPU 型号自动添加总线锁或/缓存锁
    总线锁是锁定了 CPU 的前端总线,从而导致在同一时刻只能有一个线程去和内存通信,这样就避免了多线程并发造成的可见性。
    缓存锁是对总线锁的优化,因为总线锁导致了 CPU 的使用效率大幅度下降,所以缓存锁只针对 CPU 三级缓存中的目标数据加锁,缓存锁是使用 MESI 缓存一致性来实现的。
    指令重排序,所谓重排序,就是指令的编写顺序和执行顺序不一致,在多线程环境下导致可见性问题。指令重排序本质上是一种性能优化的手段,它来自于几个方面。
    CPU 层面,针对 MESI 协议的更进一步优化去提升 CPU 的利用率,引入了StoreBuffer 机制,而这一种优化机制会导致 CPU 的乱序执行。当然为了避免这样的问题,CPU 提供了内存屏障指令,上层应用可以在合适的地方插入内存屏障来避免 CPU 指令重排序问题。
    编译器的优化,编译器在编译的过程中,在不改变单线程语义和程序正确性的前提下,对指令进行合理的重排序优化来提升性能。
    所以,如果对共享变量增加了 volatile 关键字,那么在编译器层面,就不会去触发编译器优化,同时再 JVM 里面,会插入内存屏障指令来避免重排序问题。
    当然,除了 volatile 以外,从 JDK5 开始,JMM 就使用了一种 Happens-Before模型去描述多线程之间的内存可见性问题。
    如果两个操作之间具备 Happens-Before 关系,那么意味着这两个操作具备可见性关系,不需要再额外去考虑增加 volatile 关键字来提供可见性保障。
    先来看变量的可见性,简单来说,就是指当某一个线程对共享变量的修改,其他线程可以立刻看到修改之后的值。其实这个可见性问题,我认为本质上是由以下两个方面造成的。
    首先是,CPU的高速缓存。在 CPU 里面设计了三级缓存去解决 CPU 运算效率和内存 IO 效率问题,但是有带来了缓存的一致性问题,而在多线程并行执行的情况下,缓存一致性就会导致可见性
    问题。所以,对于增加了 volatile 关键字修饰的共享变量,JVM 虚拟机会自动增加一个#Lock 汇编指令,这个指令会根据 CPU 型号自动添加总线锁或/缓存锁。
    我简单介绍一下这两种锁:
    总线锁是锁定了 CPU 的前端总线,从而导致在同一时刻只能有一个线程去和内存通信,这样就避免了多线程并发造成的可见性。
    缓存锁是对总线锁的优化,因为总线锁导致了 CPU 的使用效率大幅度下降,所以缓存锁只针对CPU 三级缓存中的目标数据加锁,缓存锁是使用 MESI 缓存一致性来实现的。
    然后,就是屏蔽指令重排,就是指屏蔽CPU指令重排序。意思是在多线程环境下,CPU指令的
    编写顺序和执行顺序不一致,从而导致可见性问题,为了提升 CPU 的利用率,CPU引入了StoreBuffer 机制,而这一种优化机制会导致 CPU 的乱序执行。当然为了避免这样的问题,CPU 提供了内存屏障指令,上层应用可以在合适的地方插入内存屏障来避免 CPU 指令重排序问题。而volatile就是通过设置内存屏障来禁止指令重排。
    volatile内存屏障实现原理主要从以下两个方面来分析:
    第1个是:volatile会在变量写操作的前后加入两个内存屏障,来保证前面的写指令和后面的读指令是有序的
    volatile在变量的读操作后面插入两个指令,禁止后面的读指令和写指令重排序。
    volatile其实可以看作是轻量级的synchronized,虽然说volatile不能保证原子性,但是如果在
    多线程下的操作本身就是原子性操作(例如赋值操作),那么使用volatile会优于synchronized。以
    上就是我对volatile 关键字的理解
    对于可见性,Java 提供了 volatile 关键字来保证可见性。当一个共享变量被 volatile 修饰时,它会保证修改的值会立即被更新到主存,当有其他线程需要读取时,它会去内存中读取新值。从实践角度而言,volatile 的一个重要作用就是和 CAS 结合,保证了原子性,详细的可以参见 java.util.concurrent.atomic 包下的类,比如 AtomicInteger。

volatile 关键字的作用

对于可见性,Java 提供了 volatile 关键字来保证可见性和禁止指令重排。 volatile 提供 happens
before 的保证,确保一个线程的修改能对其他线程是可见的。当一个共享变量被 volatile 修饰
时,它会保证修改的值会立即被更新到主内存中,当有其他线程需要读取时,它会去内存中读取新
值。
从实践角度而言,volatile 的一个重要作用就是和 CAS 结合,保证了原子性,详细的可以参见
java.util.concurrent.atomic 包下的类,比如 AtomicInteger。
volatile 常用于多线程环境下的单次操作(单次读或者单次写)。

为什么使用volatile

在某些情况下,volatile同步机制的性能要优于锁(synchronized关键字),但是由于虚拟机对锁实行的许多消除和优化,所以并不是很快。
volatile变量读操作的性能消耗与普通变量几乎没有差别,但是写操作则可能慢一些,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。
指令重排序,内存栅栏
指令重排序:编译器或运行时环境为了优化程序性能而采取的对指令进行重新排序执行的一种手段。在单线程程序中,对存在控制依赖的操作重排序,不会改变执行结果;但在多线程程序中,对存在控制依赖的操作重排序,可能会改变程序的执行结果。
指令重排序
运行结果可能为(1,0)、(0,1)或(1,1),也可能是(0,0)。因为,在实际运行时,代码指令可能并不是严格按
照代码语句顺序执行的。大多数现代微处理器都会采用将指令乱序执行(out-of-order execution,简
称OoOE或OOE)的方法,在条件允许的情况下,直接运行当前有能力立即执行的后续指令,避开获取
下一条指令所需数据时造成的等待3。通过乱序执行的技术,处理器可以大大提高执行效率。而这就是
指令重排
3)内存屏障
内存屏障,也叫内存栅栏,是一种CPU指令,用于控制特定条件下的重排序和内存可见性问题。
LoadLoad屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的
数据被访问前,保证Load1要读取的数据被读取完毕。
StoreStore屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行
前,保证Store1的写入操作对其它处理器可见。
LoadStore屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出
前,保证Load1要读取的数据被读取完毕。
StoreLoad屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行
前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。 在大多数处理器的实
现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能

为什么代码会重排序
在执行程序时,为了提供性能,处理器和编译器常常会对指令进行重排序,但 是不能随意重排序,不是你想怎么排序就怎么排序,它需要满足以下两个条 件:
在单线程环境下不能改变程序运行的结果;
存在数据依赖关系的不允许重排序
需要注意的是:重排序不会影响单线程环境的执行结果,但是会破坏多线程的 执行语义

volatile 变量和 atomic 变量有什么不同
volatile 变量可以确保先行关系,即写操作会发生在后续的读操作之前, 但它并不能保证原子性。
例如用 volatile 修饰 count 变量,那么 count++ 操作就不是原子性的。
而 AtomicInteger 类提供的 atomic 方法可以让这种操作具有原子性如getAndIncrement()方法会
原子性的进行增量操作把当前值加一,其它数据类型和引用变量也可以进行相似操作。

volatile 能使得一个非原子操作变成原子操作吗
关键字volatile的主要作用是使变量在多个线程间可见,但无法保证原子性,对于多个线程访问同 一个实例变量需要加锁进行同步。
虽然volatile只能保证可见性不能保证原子性,但用volatile修饰long和double可以保证其操作原子性。
所以从Oracle Java Spec里面可以看到:
对于64位的long和double,如果没有被volatile修饰,那么对其操作可以不是原子的。在操作的时
候,可以分成两步,每次对32位操作。
如果使用volatile修饰long和double,那么其读写都是原子操作
对于64位的引用地址的读写,都是原子操作
在实现JVM时,可以自由选择是否把读写long和double作为原子操作
推荐JVM实现为原子操作

Happens-Before模型

内存可见性问题
首先,Happens-Before是一种可见性模型,也就是说,在多线程环境下。原本因为指令重排序的存在会导致数据的可见性问题,也就是A线程修改某个共享变量对B线程不可见。
因此,JMM通过Happens-Before关系向开发人员提供跨越线程的内存可见性保证。如果一个操作的执行结果对另外一个操作可见,那么这两个操作之间必然存在Happens-Before管理。
其次,Happens-Before关系只是描述结果的可见性,并不表示指令执行的先后顺序,也就是说只要
不对结果产生影响,仍然允许指令的重排序。最后,在JMM中存在很多的Happens-Before规则。
程序顺序规则,一个线程中的每个操作,happens-before这个线程中的任意后续操作,可以简单认
为是as-if-serial也就是不管怎么重排序,单线程的程序的执行结果不能改变传递性规则,也就是A
Happens-Before B,B Happens-Before C。就可以推导出A Happens-Before C。

happen-before原则
单线程happen-before原则:在同一个线程中,书写在前面的操作happen-before后面的操作。
锁的happen-before原则:同一个锁的unlock操作happen-before此锁的lock操作。
volatile的happen-before原则:对一个volatile变量的写操作happen-before对此变量的任意操
作(当然也包括写操作了)。
happen-before的传递性原则:如果A操作 happen-before B操作,B操作happen-before C操
作,那么A操作happen-before C操作。
线程启动的happen-before原则:同一个线程的start方法happen-before此线程的其它方法。
线程中断的happen-before原则 :对线程interrupt方法的调用happen-before被中断线程的检测
到中断发送的代码。
线程终结的happen-before原则: 线程中的所有操作都happen-before线程的终止检测。
对象创建的happen-before原则: 一个对象的初始化完成先于他的fifinalize方法调用。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

打赏作者

思静语

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

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

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

打赏作者

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

抵扣说明:

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

余额充值