1000字范文,内容丰富有趣,学习的好帮手!
1000字范文 > mysql查询缓存变量 如何彻底理解volatile关键字 – 数据库 – 前端 mysql 创建用户 附权限

mysql查询缓存变量 如何彻底理解volatile关键字 – 数据库 – 前端 mysql 创建用户 附权限

时间:2020-12-25 17:43:45

相关推荐

mysql查询缓存变量 如何彻底理解volatile关键字 – 数据库 – 前端 mysql 创建用户 附权限

JMM 基础-计算机原理

Java 内存模型即Java Memory Model,简称JMM,JMM定义了Java 虚拟机(JVM)在计算机(RAM) 中的工作方式。 JVM 是整个计算机虚拟模型,所以JMM是隶属于JVM的,

在计算机系统中,寄存器是L0级缓存,接着依次是L1,L2,L3(接下来是内存,本地磁盘,远程存储).越往上的缓存存储空间越小,速度越快,成本也越高。越往下的存储空间越大,速度更慢,成本也越低。

从上至下,每一层都都可以是看作是更下一层的缓存,即:L0寄存器是L1一级缓存的缓存,

L1是L2的缓存,一次类推;每一层的数据都是来至于它的下一层。

在现在CPU上,一般来说L0,L1,L2,L3都继承在CPU内部,而L1还分为一级数据缓存和一级指令缓存,分别用于存放数据和执行数据的指令解码,每个核心拥有独立的运算处理单元、控制器、寄存器、L1缓存、L2 缓存,然后一个CPU的多个核心共享最后一层CPU缓存L3。

CPU 的缓存一致性解决方案

分为以下两种方案

总线锁(每次锁总线,是悲观锁)

缓存锁(只锁缓存的数据)

MESI协议如下:

M(modify):I(invalid)E(Exclusive)S(Share)JMM内存模型的八种同步操作

1、read(读取),从主内存读取数据

2、load(载入):将主内存读取到的数据写入到工作内存

3、use(使用): 从工作内存读取数据来计算

4、assign(赋值):将计算好的值重新赋值到工作内存中

5、store(存储):将工作内存数据写入主内存

6、write(写入):将store过去的变量值赋值给主内存中的变量

7、lock(锁定):将主内存变量加锁,标识为线程 独占状态

8、unlock(解锁):将主内存变量解锁,解锁后其他线程可以锁定该变量

Java 内存模型带来的问题

1、可见性问题

左边CPU中运行的线程从主内存中拷贝对象obj到它的CPU缓存,把对象obj的count变量改为2,但这个变更对运行在右边的CPU中的线程是不可见,因为这个更改还没有flush到主内存中。

在多线程环境下,如果某个线程首次读取共享变量,则首先到主内存中获取该变量,然后存入到工作内存中,以后只需要在工作内存中读取该变量即可,同样如果对该变量执行了修改的操作,则先将新值写入工作内存中,然后再刷新至于内存中,但是什么时候最新的值会被刷新到主内存中是不太确定的,一般来说是很快的,但是具体时间未知,,要解决共享对象可见性问题,大家可以使用volatile关键字或者加锁。

2、竞争问题

线程A 和 线程B 共享一个对象obj, 假设线程A从主存读取obj.count变量到自己的缓存中,同时,线程B也读取了obj.count变量到它的CPU缓存,并且这两个线程都对obj.count做了加1操作,此时,obj.count加1操作被执行了两次,不过都在不同的CPU缓存中。

如果则两个加1操作是串行执行的,那么obj.count变量便会在原始值上加2,最终主内存中obj.count的值会为3,然后图中两个加1操作是并行的,不管是线程A还是线程B先flush计算结果到主存,最终主存中的obj.count只会增加1次变成2,尽管一共有两次加1操作,要解决上面的问题大家可以使用synchronized 代码块。

3、重排序

除了共享内存和工作内存带来的问题,还存在重排序的问题,在执行程序时,为了提高性能,编译器和处理器常常会对指令做重排序。

重排序分3中类型:

(1) 编译器优化的重排序。

(2) 指令级并行的重排序

(3)内存系统的重排序

① 数据依赖性

数据依赖性: 如果两个操作访问同一变量,且这两个操作中有一个为写,此时这两个操作之间就存在数据依赖性。

依赖性分为以下三种:

上图很明显,A和C存在数据依赖,B和C也存在数据依赖,而A和B之间不存在数据依赖,如果重排序了A和C或者B和C的执行顺序,程序的执行结果就会被改变。

很明显,不管如何重排序,都必须保证代码在单线程下的运行正确,连单线程下都无法保证,更不用讨论多线程并发的情况,所以就提出一个as – if -serial 的概念。

4、as – if -serial

意思是:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as – if -serial 语义。

A和C之间存在数据依赖,同时B和C之间也存在数据依赖关系,因此在最终执行的指令序列中,C不能被重排序A和B的前面(C排到A和B的前面,程序的结果将会被改变)。但A和B之间没有数据依赖关系,编译器和处理器可以重排序A和B之间的执行顺序。

as – if -serial 语义把单线程程序保护了起来,遵守as-if-serial语义的编译器、runtime和处理器可以让大家感觉到: 单线程程序看起来是按程序的顺序来执行的。as-if-srial语义使单线程程序无需担心重排序干扰他们,也无需担心内存可见性的问题。

5、内存屏障

Java 编译器在生成指令序列的适当位置会插入内存屏障来禁止特定类型的处理去重排序,从而让程序按大家预想的流程去执行。

① 保证特定操作的执行顺序

② 影响某些数据(或者是某条指令的执行结果)的内存可见性

编译器和CPU能够重排序指令,保证最终相同的结果,尝试优化性能。插入一条Memory Barrier 会告诉编译器和CPU ,不管什么指令都不能和这条Memory Barrier 指令重排序。

Memory Barrier 所做的另外一件事是强制刷出各种CPU cache, 如一个Write-Barrier(写入屏障)将刷出所在的Barrier 之前写入cache的数据,因此,任何CPU上的线程都能读取到这些数据的最新版本。

JMM把内存屏障指令分为4类:

StoreLoad Barriers 是一个”全能型”的屏障,它同时具有其他3个屏障的效果,

volatile 关键字介绍

1、保证可见性

对一个volatile变量的读,总是能看到(任意线程)对这个volatile变量最后的写。

大家先看下面代码:

initFlag 没有用volatile关键字修饰;

上面结果为:

说明一个线程改变initFlag状态,另外一个线程看不见;

如果加上volatile关键字呢?

结果如下:

大家通过汇编看下代码的最终底层实现:

volatile写的内存语义如下:

当写一个volatile变量时,JMM会把该线程对应的本地内存中的共享变量值刷新到主内存。

当读一个volatile变量时,JMM会把该线程对应的本地内存置为无效。线程接下来将从主内存中读取共享变量。

比如:

如果大家将flag变量以volatile关键字修饰,那么实际上:线程A在写flag变量后,本地内存A中被线程A更新过的两个共享变量的值都被刷新到主内存中。

在读flag变量后,本地内存B包含的值已经被置为无效。此时,线程B必须从主内存中读取共享变量。线程B的读取操作将导致本地内存B与主内存中的共享变量的值变成一致。

如果大家把volatile写和volatile读两个步骤综合起来看的话,在读线程B读一个volatile变量后,写线程A在写这个volatile变量之前所有可见的共享变量的值都将立即变得对读线程B可见。

2、原子性

volatile 不保证变量的原子性;

运行结果如下:

因为count ++;

包含 三个操作:

(1) 读取变量count

(2) 将count变量的值加1

(3) 将计算后的值再赋给变量count

从JMM内存分析:

下面从字节码分析为什么i++这种的用volatile修改不能保证原子性?

javap : 字节码查看

其实i++这种操作主要可以分为3步:(汇编)

读取volatile变量值到local增加变量的值把local的值写回,让其它的线程可见

Load到store到内存屏障,一共4步,其中最后一步jvm让这个最新的变量的值在所有线程可见,也就是最后一步让所有的CPU内核都获得了最新的值,但中间的几步(从Load到Store)是不安全的,中间如果其他的CPU修改了值将会丢失。

3、有序性

(1) volatile重排序规则表

① 当第二个操作是volatile写时,不管第一个操作是什么,都不能重排序。这个规则确保volatile写之前的操作不会被编译器重排序到volatile写之后。

② 当第一个操作是volatile读时,不管第二个操作是什么,都不能重排序。这个规则确保volatile读之后的操作不会被编译器重排序到volatile读之前。

③ 当第一个操作是volatile写,第二个操作是volatile读时,不能重排序。

(2) volatile的内存屏障

① volatile写

storestore屏障:对于这样的语句store1; storestore; store2,在store2及后续写入操作执行前,保证store1的写入操作对其它处理器可见。(也就是说如果出现storestore屏障,那么store1指令一定会在store2之前执行,CPU不会store1与store2进行重排序)

storeload屏障:对于这样的语句store1; storeload; load2,在load2及后续所有读取操作执行前,保证store1的写入对所有处理器可见。(也就是说如果出现storeload屏障,那么store1指令一定会在load2之前执行,CPU不会对store1与load2进行重排序

② volatile读

在每个volatile读操作的后面插入一个LoadLoad屏障。在每个volatile读操作的后面插入一个loadstore屏障。

loadload屏障:对于这样的语句load1; loadload; load2,在load2及后续读取操作要读取的数据被访问前,保证load1要读取的数据被读取完毕。(也就是说,如果出现loadload屏障,那么load1指令一定会在load2之前执行,CPU不会对load1与load2进行重排序)

loadstore屏障:对于这样的语句load1; loadstore; store2,在store2及后续写入操作被刷出前,保证load1要读取的数据被读取完毕。(也就是说,如果出现loadstore屏障,那么load1指令一定会在store2之前执行,CPU不会对load1与store2进行重排序)

volatile的实现原理

volatile的实现原理

❶ 通过对OpenJDK中的unsafe.cpp源码的分析,会发现被volatile关键字修饰的变量会存在一个“lock:”的前缀。

❷ Lock前缀,Lock不是一种内存屏障,但是它能完成类似内存屏障的功能。Lock会对CPU总线和高速缓存加锁,可以理解为CPU指令级的一种锁。

❸ 同时该指令会将当前处理器缓存行的数据直接写会到系统内存中,且这个写回内存的操作会使在其他CPU里缓存了该地址的数据无效。

❹ 具体的执行上,它先对总线和缓存加锁,然后执行后面的指令,最后释放锁后会把高速缓存中的脏数据全部刷新回主内存。在Lock锁住总线的时候,其他CPU的读写请求都会被阻塞,直到锁释放。

【欢迎随手关注@码农的一天,希望对你有帮助】

本内容不代表本网观点和政治立场,如有侵犯你的权益请联系我们处理。
网友评论
网友评论仅供其表达个人看法,并不表明网站立场。