volatile底层原理详解
volatile底层原理详解
-
使用方式
可以用于修饰变量,不能用于修饰方法、代码块
-
特性
可见性:保证被volatile修饰的共享变量对所有线程都是可见的,也就是当一个线程修改了一个被volatile修饰的变量的值,新值总是可以被其他线程立即得知
有序性:禁止指令重排序优化
-
volatile如何保证可见性
要解释这个问题,需要先了解线程内存模型。
线程内存模型
Sequential Consistency Memory Model:顺序一致性模型。这个模型定义了程序执行的顺序和代码执行的顺序是一致的。也就是说,一个线程T1对共享变量A进行写操作,另外一个线程T2对A进行读操作。如果T1在时间上先于T2执行,那么T2就可以看见T1修改之后的值。
缺点:每次对共享变量的修改都要立刻同步回主内存,不能把变量保存到处理器寄存器或者处理器缓存中。在多处理并发执行程序的时候,会严重的影响程序的性能。
Happens-Before Memory Model:先行发生模型。如果有两个操作A和B存在A Happens-Before B,那么操作A对变量的修改对操作B来说是可见的。这个先行并不是代码执行时间上的先后关系,而是保证执行结果是顺序的。遵循Happens-Before原则。
Java线程内存模型(JMM)是建立在先行发生模型上,并进行了增强。
什么是Happens-Before原则?
具体的定义为:
1)如果一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。
2)两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么这种重排序并不非法(也就是说,JMM允许这种重排序)。
具体的规则为:
- 程序顺序规则: 一个线程中的每个操作,Happens-Before于该线程中的任意后续操作
- 监视器锁规则: 对一个锁的解锁,Happens-Before于随后对这个锁的加锁。
- volatile变量规则:对一个volatile域的写,Happens-Before于任意后续对这个volatile域的读。
- 传递性:如果A Happens-Before B,且B Happens-Before C,那么 A Happens-Before C。
- start()规则:如果线程A执行操作Thread.start() (启动线程B),那么A线程的Thread.start() 操作Happens-Before于B中任意的操作。
- Join规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-before于线程A从ThreadB.join()操作成功返回
- 程序中断规则:对线程interrupted()方法的调用先于被中断线程的代码检测到中断时间的发生
- 对象finalize规则:一个对象的初始化完成(构造函数执行结束)先行于发生它的finalize()方法的开始。
那么Java线程内存模型(JMM)又是什么呢?
首先说明JMM是一个抽象概念,是一组规则,并不实际存在。它是标准化的,屏蔽了底层不同计算机的区别。
JMM模型跟CPU缓存模型结构类似。
![]()
Java内存模型内存交互操作
- lock(锁定):作用于主内存的变量,把一个变量标记为一条线程独占状态
- unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其它线程锁定
- read(读取):作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存中,以便随后load动作使用
- load(载入):作用于工作内存中的变量,它把read操作从主内存中得到的变量放入工作内存中的变量副本中
- use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎
- assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量
- store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作。
- write(写入):作用于工作内存的变量,它把store操作从工作内存中的一个变量的值传递到主内存的变量中
把一个变量从主内存中复制到工作内存中,就需要顺序地执行read和load操作,如果把变量从工作内存中同步到主内存中,就需要按顺序地执行store和write操作。但Java内存模型只要求上述8大操作(原子操作)必须按顺序执行,而没有保证必须是连续执行。
![]()
Java内存模型内存同步规则:
- 不允许一个线程无原因地(没有发生过任何assing操作)把数据从工作内存同步回主内存中
- 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或者assign)的变量。即就是对一个变量实施use和store操作之前,必须先自行assign和Load操作
- 一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。lock和unlock必须成对出现。
- 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量之前需要重新执行load或assign操作初始化变量的值
- 如果一个变量实现没有被lock操作锁定,则不允许对他执行unlock操作;也不允许去unlock一个被其他线程锁定的变量
- 对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)
缓存一致性协议
![]()
什么是锁总线,锁缓存行?
总线是CPU和内存之间的连接工作区域
缓存行可以理解为CPU缓存中的最小缓存单位,目前主流CPU的缓存行都是64字节。CPU高速缓存存在缓存不一致问题,解决方法是加锁。
- 将锁加在总线上,每次缓存和主内存同步数据都要锁总线,锁总线会导致线程停止工作,将会严重影响性能。
- 将锁加在缓存行上,这个缓存行上数据需要同步,只会影响阻塞操作这一个缓存行的线程,不会影响其他线程,性能影响不大。
![]()
伪共享:在Java中一个long型数据占8个字节,一个缓存行可以有8个long型数据。这是有多个线程分别操作这8个数据,这时一个线程获取这个缓存行,会将其他线程中持有这个缓存行中数据的状态设为无效,其他线程操作他们的数据时,就会重新抢夺这个缓存行的所有权。这种状况会导致缓存在失效,浪费系统资源。
Java中的解决方法:
- 用其他无效变量填充满这个缓存行。例如定义了一个有用得long型数据,可以再另外新增7个无操作的long数据。
- 在JDK8之后,新增了一个注解@sun.misc.Contended。直接在变量上添加这个注解。(注:JVM 添加 -XX:-RestrictContended 参数后 @sun.misc.Contended 注解才有效)
适用场景:主要适用于频繁写的共享数据上。如果不是频繁写的数据,那么 CPU 缓存行被锁的几率就不多,所以没必要使用了,否则不仅占空间还会浪费 CPU 访问操作数据的时间。
了解完以上知识点,就能解释volatile如何保证可见性了。
- JMM内存交互层面:volatile修饰的变量的read、load、use操作和assign、load、write操作必须是连续的,即修改后必须立即同步回主内存,使用时必须从主内存刷新,由此保证volatile变量的可见性
- 底层实现:通过汇编lock前缀指令,它会锁定变量缓存行区域并写回主内存,这个操作称为“缓存锁定”,缓存一致性机制会阻止同时修改被两个以上处理器缓存的内存区域数据。一个处理器的缓存回写到内存,内存会导致其他处理器的缓存无效。
-
volatile如何保证有序性
禁止指令重排序。
什么是指令重排序?
只要程序的最终结果与它顺序化情况的结果相等,那么指令的执行顺序可以与代码顺序不一致,这个过程叫做指令重排序。
指令重排序的意义:JVM能根据处理器特性适当的对机器指令进行重排序,是机器指令更符合CPU的执行特性,最大限度的发挥机器性能。
在编译器与CPU处理器中都能执行指令重排序优化操作。JMM内存屏障插入策略:
- 在每个volatile写操作的前面插入一个StoreStore屏障
- 在每个volatile写操作的后面插入一个StoreLoad屏障
- 在每个volatile读操作的后面插入一个LoadLoad屏障
- 在每个volatile读操作的后面插入一个LoadStore屏障
![]()
![]()







浙公网安备 33010602011771号