当前位置:网站首页>Analysis of volatile principle
Analysis of volatile principle
2022-08-02 01:55:00 【Ysming88】
volatile基本介绍
volatile 修饰变量,保证可见性与有序性,但是不保证原子性,保证原子性需要借助 synchronized 这样的锁机制. 所以我们主要围绕着这三个特点来了解 volatile.
volatile关键字虽然从字面上理解起来比较简单,但是要用好不是一件容易的事情.由于volatile关键字是与Java的内存模型有关的,因此在讲述volatile关键之前,我们先来了解一下与内存模型相关的概念和知识,然后分析了volatile关键字的实现原理.
JMM:Java内存模型
JMM Java 内存模型,它是一种抽象的概念并不真实存在,它描述的是一组规则或规范,通过这组规范定义了程序中各个变量的访问方式.
Java Memory Model(Java内存模型),It's around how visibility is handled in a concurrent process、原子性、有序性这三个特性而建立的模型.
内存可见性
在Java中,Different threads have their ownPrivate working memory,当线程需要读取或修改某个变量时,不能直接去操作主内存中的变量,而是需要将这个变量读取到线程的A copy of the variable in working memory中,当该线程修改其变量副本的值后,其它线程并不能立刻读取到新值,需要将修改后的值刷新到主内存中,其它线程才能从主内存读取到修改后的值.
关于JMM的一些同步的约定:
1、线程解锁前,必须把共享变量立刻刷回主存.
2、线程加锁前,必须读取主存中的最新值到工作内存中!
3、加锁和解锁是同一把锁
内存交互操作有8种,虚拟机实现必须保证每一个操作都是原子的,不可在分的(对于double和long类型的变量来说,load、store、read和write操作在某些平台上允许例外)
lock (锁定):作用于主内存的变量,把一个变量标识为线程独占状态
unlock (解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
read (读取):作用于主内存变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用
load (载入):作用于工作内存的变量,它把read操作从主存中变量放入工作内存中
use (使用):作用于工作内存中的变量,它把工作内存中的变量传输给执行引擎,每当虚拟机遇到一个需要使用到变量的值,就会使用到这个指令
assign (赋值):作用于工作内存中的变量,它把一个从执行引擎中接受到的值放入A copy of the variable in working memory中
store (存储):作用于主内存中的变量,它把一个从工作内存中一个变量的值传送到主内存中,以便后续的write使用
write (写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中
JMM对这八种指令的使用,制定了如下规则:
- 不允许read和load、store和write操作之一单独出现.即使用了read必须load,使用了store必须write
- 不允许线程丢弃他最近的assign操作,即工作变量的数据改变了之后,必须告知主存
- 不允许一个线程将没有assign的数据从工作内存同步回主内存
- 一个新的变量必须在主内存中诞生,不允许工作内存直接使用一个未被初始化的变量.就是对变量实施use、store操作之前,必须经过assign和load操作
- 一个变量同一时间只有一个线程能对其进行lock.多次lock后,必须执行相同次数的unlock才能解锁
- 如果对一个变量进行lock操作,会清空所有工作内存中此变量的值,在执行引擎使用这个变量前,必须重新load或assign操作初始化变量的值
- 如果一个变量没有被lock,就不能对其进行unlock操作.也不能unlock一个被其他线程锁住的变量
- 对一个变量进行unlock操作之前,必须把此变量同步回主内存
一致性协议不用去锁总线,只要你设置了 volatile,那么就有一个 lock 指令触发缓存失效
JMM三个特征
- 原子性(Atomicity):一个操作不能被打断,要么全部执行完毕,要么不执行.在这点上有点类似于事务操作,要么全部执行成功,要么回退到执行该操作之前的状态.
- 可见性:一个线程对共享变量做了修改之后,其他的线程立即能够看到(感知到)该变量的这种修改(变化).
- 有序性:对于一个线程的代码而言,我们总是以为代码的执行是从前往后的,依次执行的.这么说不能说完全不对,在单线程程序里,确实会这样执行;但是在多线程并发时,程序的执行就有可能出现乱序(编译器优化的重排)
深入剖析volatile关键字
可见性
volatile保证变量的可见性,如何保证可见性的,我们不得不提到 MESI 缓存一致性协议.
MESI 缓存一致性协议:多个 cpu 主内存读取同一个数据到各自的高速缓存,当其中某个 cpu 修改了缓存里的数据,该数据会马上同步回主内存,其它 cpu 通过总线嗅探机制可以感知到数据的变化从而将自己缓存里的数据失效.
那这个总线嗅探机制又是什么呢?每个 cpu 都会对总线进行一个监听.如果我们一个线程修改了变量值,那它写回主内存的路径上一定会经过总线.那么如果其他线程一直对总线进行监听,当发现改值发生变化,It will empty the variable value in its own working memory,然后重新去主内存读取该变量值.
Volatile 缓存可见性实现原理 汇编指令 lock(实际上相当于一个内存屏障)
- After the thread changes the variable in its own memory,当前处理器缓存行数据立刻写回主内存(如果不加volatile,It will appear after the thread modifies the value of the variable,但是还没来得及写入主存当中)
- 这个写操作,会触发总线嗅探机制(MESI 协议)
解释一下就是它的可见性的实现原理是 底层实现主要通过汇编 Lock 前缀指令(变量加了 volatile 当修改操作时,底层汇编会给该行加一个 lock 锁),他会锁定这块内存区域的缓存(缓存行锁定)并写回到主内存.而其他线程的工作内存又时刻对总线进行监听,监听到该变量发生变化会引起其他 cpu 里缓存了该内存地址的数据无效(MESI协议).如果要想获取该值,就要重新去主内存获取.
不保证原子性
public class Test {
public volatile int inc = 0;
public void increase() {
inc++;
}
public static void main(String[] args) {
final Test test = new Test();
for(int i=0;i<10;i++){
new Thread(){
public void run() {
for(int j=0;j<1000;j++)
test.increase();
};
}.start();
}
while(Thread.activeCount()>1) //保证前面的线程都执行完
Thread.yield();
System.out.println(test.inc);
}
}
大家想一下这段程序的输出结果是多少?也许有些朋友认为是10000.但是事实上运行它会发现每次运行结果都不一致,都是一个小于10000的数字.
可能有的朋友就会有疑问,不对啊,上面是对变量inc进行自增操作,由于volatile保证了可见性,那么在每个线程中对inc自增完之后,在其他线程中都能看到修改后的值啊,所以有10个线程分别进行了1000次操作,那么最终inc的值应该是1000*10=10000.
为什么会少了呢?这就是 volatile 的不保证原子性.那么接下来我们分析一下原因.
Some blogs on the Internet explain that:
假设某一时刻 i=5,At this point there are two threads reading from main memory at the same time i 的值,Then the two threads are saved at this time i 的值都是 5, 此时 A 线程对 i An auto-increment calculation is performed,然后 B 也对 i Do an auto-increment calculation,At this point, the two threads are finally flushed back to main memory i 的值都是 6(Originally, the two threads should be calculated after the calculation 7)所以说 volatile 保证不了原子性.
My confusion:
既然 i 是被 volatile 修饰的变量,那么对于 i The operation should be visible between threads,就算 A.,B Both threads read at the same time i 的值是 5,但是如果 A 线程执行完 i The operation should be put later B 线程读到的 i The value of is invalidated and forced B 重新读入 i is the new value of 6 Then the auto-increment operation will be performed.
Later, referring to other blogs, I finally figured it out:
因为volatileThe atomicity of variable operations is not guaranteed
So threads are implemented in their own memory+1的操作不是原子性的,It is divided into the following steps
1、线程读取 i
2、temp = i + 1
3、i = temp
当 i=5 的时候 A,B Both threads read at the same time i 的值, 然后 A 线程执行了 temp = i + 1 的操作, 要注意,此时的 i 的值还没有变化,然后 B 线程也执行了 temp = i + 1 的操作,注意,此时 A,B Two threads are saved i 的值都是 5,temp 的值都是 6, 然后 A 线程执行了 i = temp (6)的操作,此时 i 的值会立即刷新到主存并通知其他线程保存的 i 值失效, 此时 B The thread needs to reread i The value then at this time B 线程保存的 i 就是 6,同时 B 线程保存的 temp 还仍然是 6, 然后 B 线程执行 i=temp (6),所以导致了计算结果比预期少了 1.
那么想保证原子性吗,简单,Add one before the variable sychronized,或者加锁Lock,以及用AtomicInteger.
有序性
volatile保证了有序性,i.e. banned指令重排,计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序,一般分为以下三种:
源代码 ——> 编译器优化的重排 ——> 指令并行的重排 ——> 内存系统的重排 ——> 最终执行的指令
其中,在单线程环境里面确保程序最终执行结果和代码顺序执行的结果一致. 而多线程环境中线程交替执行,由于编译器优化重排的存在,两个线程中使用的变量能否保证一致性是无法确定的,结果无法预测.处理器在进行重排序时必须要考虑指令之间的数据依赖性.
volatile 实现禁止指令重排优化,从而避免多线程环境下出现乱序执行的现象.
先了解一个概念,内存屏障(Memory Barrier),是一个 CPU 指令,它的作用有两个:
- 保持特定操作的执行顺序
- 保持某些变量的内存可见性
由于编译器和处理器都能执行指令重排优化.如果在指令间插入一条 Memory Barrier 则会告诉编译器和 CPU,不管什么指令都不能和这条 Memory Barrier 指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化.内存屏障另外一个作用是强制刷出各种 CPU 的缓存数据,因此任何 CPU 上的线程都能读取到这些数据的最新版本.
为此,Java 内存模型采取的策略为 在每个volatile写操作的前面插入一个StoreStore屏障. 在每个volatile写操作的后面插入一个StoreLoad屏障. 在每个volatile读操作的后面插入一个LoadLoad屏障. 在每个volatile读操作的后面插入一个LoadStore屏障.
写操作
读操作
通过屏障,我们可以做到其他代码不会干扰到内存屏障内的代码.
volatile的原理和实现机制
前面讲述了源于volatile关键字的一些使用,下面我们来探讨一下volatile到底如何保证可见性和禁止指令重排序的.
下面这段话摘自《深入理解Java虚拟机》:
“观察加入volatile关键字和没有加入volatile关键字时所生成的汇编代码发现,加入volatile关键字时,会多出一个lock前缀指令”
lock前缀指令实际上相当于一个内存屏障(也成内存栅栏),内存屏障会提供3个功能:
1)它确保指令重排序时不会把其后面的指令排到内存屏障之前的位置,也不会把前面的指令排到内存屏障的后面;即在执行到内存屏障这句指令时,在它前面的操作已经全部完成;
2)它会强制将对缓存的修改操作立即写入主存;
3)如果是写操作,它会导致其他CPU中对应的缓存行无效
总结:
今天呢,我给大家介绍了 volatile,它主要有三个重要的点,分别是可见性,有序性和不保证原子性.而可见性是通过 JMM 模型对变量访问方式的规定及 MESI(缓存一致性协议)的嗅探机制来实现的;有序性是通过内存屏障,在汇编的层面对需要保证有序性的代码前后加屏障来保证代码的有序执行;最后又分析了 volatile 在循环类似 i++ 的情况下会出现不保证原子性的情况.
参考博客:
https://blog.51cto.com/u_15138908/2668818
https://blog.csdn.net/weixin_42146366/article/details/108125904
https://blog.csdn.net/github_37130188/article/details/102673488
边栏推荐
- 数据链路层的数据传输
- Shell Beginners Final Chapter
- 【ORB_SLAM2】void Frame::AssignFeaturesToGrid()
- 3.Bean的作用域与生命周期
- typescript30 - any type
- 密码学的基础:X.690和对应的BER CER DER编码
- 传统企业数字化转型需要经过几个阶段?
- For effective automated testing, these software testing tools must be collected!!!
- Named parameter implementation of JDBC PreparedStatement
- AOF重写
猜你喜欢
使用百度EasyDL实现厂区工人抽烟行为识别
The Paddle Open Source Community Quarterly Report is here, everything you want to know is here
3. Bean scope and life cycle
YGG 公会发展计划第 1 季总结
『网易实习』周记(二)
Basic use of typescript34-class
软件测试功能测试全套常见面试题【开放性思维题】面试总结4-3
kubernetes之服务发现
6-24漏洞利用-vnc密码破解
Yunhe Enmo: Let the value of the commercial database era continue to prosper in the openGauss ecosystem
随机推荐
C语言之插入字符简单练习
LeetCode刷题日记:LCP 03.机器人大冒险
Reflex WMS中阶系列6:对一个装货重复run pick会有什么后果?
手写一个博客平台~第三天
Force buckle, 752-open turntable lock
PHP直播源码实现简单弹幕效果的相关代码
关于MySQL的数据插入(高级用法)
Shell Beginners Final Chapter
MySQL8 下载、启动、配置、验证
Day115. Shangyitong: Background user management: user lock and unlock, details, authentication list approval
牛顿定理和相关推论
一本适合职场新人的好书
Byte taught me a hard lesson: When a crisis comes, you don't even have time to prepare...
MySQL optimization strategy
Pcie the inbound and outbound
60种特征工程操作:使用自定义聚合函数【收藏】
Day116.尚医通:预约挂号详情 ※
Understand the big model in seconds | 3 steps to get AI to write a summary
Use baidu EasyDL implement factory workers smoking behavior recognition
Flex layout in detail