六、JAVA多线程夯实基础--Java内存模型(Java Memory Model)简称J M M和深入解析volatile关键字

1.硬件内存模型

1.1CPU缓存结构

程序是指令与数据的集合,计算机执行程序时,是C P U在执行每条指令,因为C P U要从内存读指令,又要根据指令指示去内存读写数据做运算,所以执行指令就免不了与内存打交道,早期内存读写速度与C P U处理速度差距不大,倒没什么问题。

随着C P U技术快速发展,C P U的速度越来越快,内存却没有太大的变化,导致内存的读写(IO)速度与C P U的处理速度差距越来越大,为了解决这个问题,引入了缓存(Cache)的设计,在C P U与内存之间加上缓存层,这里的缓存层就是指C P U内的寄存器与高速缓存L1,L2,L3

图中可以看出离C P U越近,存储空间越大速度越慢

1.2 CPU 缓存与内存交互

C P U运行时,会将指令与数据从主存复制到缓存层,后续的读写与运算都是基于缓存层的指令与数据,运算结束后,再将结果从缓存层写回主存。C P U基本都是在和缓存层打交道,采用缓存设计弥补主存与C P U处理速度的差距,这种设计不仅仅体现在硬件层面,在日常开发中,那些并发量高的业务场景都能看到,但是凡事都有利弊,缓存虽然加快了速度,同样也带来了在多线程场景存在的缓存一致性问题。

1.3内存屏障 Memory Barrier(Memory Fence)

(volatile 原理是依据内存屏障的)

  • 可见性

写屏障(sfence)保证在该屏障之前的,对共享变量的改动,都同步到主存当中

而读屏障(lfence)保证在该屏障之后,对共享变量的读取,加载的是主存中最新数据

  • 有序性

写屏障会确保指令重排序时,不会将写屏障之前的代码排在写屏障之后

读屏障会确保指令重排序时,不会将读屏障之后的代码排在读屏障之前

2.JAVA内存模型

J M M 是建立在硬件内存模型基础上的抽象模型,并不是物理上的内存划分,简单说,为了使Java虚拟机(Java Virtual Machine,J V M)在各平台下达到一致的内存交互效果,需要屏蔽下游不同硬件模型的交互差异,统一规范,为上游提供统一的使用接口。J M M是保证J V M在各平台下对计算机内存的交互都能保证效果一致的机制及规范。

Java内存模型规定了所有的变量都存储在主内存(Main Memory)中(此处的主内存与介绍物理硬件时提到的主内存名字一样,两者也可以类比,但物理上它仅是虚拟机内存的一部分),

每条线程还有自己的工作内存(Working Memory,可与前面讲的处理器高速缓存类比),线程的工作内存中保 存了被该线程使用的变量的主内存副本,线程对变量的所有操作(读取、赋值等)都必须在工作内 存中进行,而不能直接读写主内存中的数据。不同的线程之间也无法直接访问对方工作内存中的变 量,线程间变量值的传递均需要通过主内存来完成。

J M M抽象结构划分为线程本地缓存主存,每个线程均有自己的本地缓存,本地缓存是线程私有的,主存则是计算机内存,它是共享的。

 

2.1可见性

当一个线程修改了共享变量的值,其他线程能够立即得知这个修改,这就是可见性,如果无法保证,就会出现缓存一致性的问题J M M规定,所有的变量都放在主存中,当线程使用变量时,先从缓存中获取,缓存未命中,再从主存复制到缓存,最终导致线程操作的都是自己缓存中的变量

缓存一致性问题举例子:

 

AB两个线程执行完后,线程A与线程B缓存数据不一致,这就是缓存一致性问题,一个是1,另一个是2,如果线程A再进行一次+1操作,写入主存的还是2,也就是说两个线程对a共进行了3+1,期望的结果是3,最终得到的结果却是2

解决缓存一致性问题,就要保证可见性:变量写入主存后,把其他线程缓存的该变量清空,这样其他线程缓存未命中,就会去主存加载。

 AB两个线程执行完后,线程A缓存是空的,此时线程A再进行一次+1操作,会从主存加载(先从缓存中获取,缓存未命中,再从主存复制到缓存)得到2,最后写入主存的是3Java中提供了volatile修饰变量保证可见性。看似问题都解决了,然而上面描述的场景是建立在理想情况(线程有序的执行),实际中线程可能是并发(交替执行),也可能是并行,只保证可见性仍然会有问题,所以还需要保证原子性。

2.2原子性

原子性是指一个或者多个操作在C P U执行的过程中不被中断的特性,要么执行,要不执行,不能执行到一半。

int a=0;//原子性操作:int a=0只有一步操作,就是赋值
a++;//非原子操作:a++有三步操作,读取值、计算、赋值

如果多线程场景进行a++操作,仅保证可见性,没有保证原子性,同样会出现问题。

为了解决此问题,只要把多个操作变成一步操作,即保证原子性。


Java中提供了synchronized同时满足有序性、原子性、可见性)可以保证结果的原子性(注意这里的描述),因为synchronized可以对代码片段上锁,防止多个线程并发执行同一段代码。

2.3有序性

JVM 会在不影响正确性的前提下,可以调整语句的执行顺序,这种叫做指令重排

重排遵循as-if-serial原则,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果(即不管怎么重排序,单线程程序的执行结果不能被改变),下面这种情况,就属于数据依赖。 

int i = 10
int j = 10
//这就是数据依赖,int i 与 int j 不能排到 int c下面去
int c = i + j

但也仅仅只是针对单线程,多线程场景可没这种保证,假设A、B两个线程,线程A代码段无数据依赖,线程B依赖线程A的结果,如下图(假设保证了可见性).

为解决重排序,使用Java提供的volatile修饰变量同时保证可见性、有序性,被volatile修饰的变量会加上内存屏障禁止排序

 

3.深入解析volatile关键字

https://blog.csdn.net/fengyuyeguirenenen/article/details/122479678?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522168627079416800225569934%2522%252C%2522scm%2522%253A%252220140713.130102334..%2522%257D&request_id=168627079416800225569934&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~top_positive~default-1-122479678-null-null.142^v88^control_2,239^v2^insert_chatgpt&utm_term=volatile%E5%85%B3%E9%94%AE%E5%AD%97%E7%9A%84%E4%BD%9C%E7%94%A8java&spm=1018.2226.3001.4187

volatile内存屏障

https://blog.csdn.net/Swofford/article/details/122219749?ops_request_misc=%257B%2522request%255Fid%2522%253A%2522168627292216800182778465%2522%252C%2522scm%2522%253A%252220140713.130102334.pc%255Fall.%2522%257D&request_id=168627292216800182778465&biz_id=0&utm_medium=distribute.pc_search_result.none-task-blog-2~all~first_rank_ecpm_v1~rank_v31_ecpm-1-122219749-null-null.142^v88^control_2,239^v2^insert_chatgpt&utm_term=voletile%E6%8C%87%E4%BB%A4%E9%87%8D%E6%8E%92%E5%BA%8F&spm=1018.2226.3001.4187

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值