操作系统内存划分
寄存器->一级缓冲区(分为数据缓冲区,指令缓冲区)->二级缓冲区->三级缓冲区->内存->磁盘->远程文件服务器,缓冲区按照缓冲行存储与读取
物理内存模型带来的问题
因为每个cpu核心都享有自己的缓冲区,所以实际上在数据操作后,会先写到自己的缓冲区内,然后再刷新回主存,至于什么时候刷新是个不确定的因素。因此,在多线程环境下,对两个变量进行操作时,有可能导致另一个线程获取的数据不是最新的,可以采用加锁或者volatile关键字来保证数据的一致性。
伪共享
已知cpu在对数据存取的时候,是按照缓冲行的大小进行读取的,也就是说,会把cache,实际划分成很多个缓冲行大小,在每一次读取一个缓冲行的时候,会把整个缓冲行加载到缓冲区,因此可能在此缓冲区中有多个变量(假设有变量a,b),如果线程A和线程B同时读取了该缓冲行(cache2)到自己的cache1,此时线程A修改变量a,线程B修改了b,再写回到cache2时,就会将其中一个变量进行覆盖,所以这就叫做伪共享。解决伪共享的方式,采用数据填充,即补位,如果一个缓冲行不满,那么就进行数据填充。在concurrentHashMap中采用了sum.misc.Contended注解,但是需要启动参数上加上-XX:-RestrictContened才能生效。
JMM内存模型
从抽象的角度来看,Java内存模型与操作系统之间有着很相似的概念,即在操作系统中,内存(RAM)与JVM中的堆相似,高速缓冲区与Java中栈内存相似,cpu与Java中线程相似。因此,Java中内存模型其实是一个抽象的概念,实际上栈内存并不存在。它涵盖了缓存、写缓冲区、寄存器以及其他硬件和编译器优化。
JMM导致的并发安全问题
引发可见性、原子性问题。
由JMM内存模型可发现,实际上线程是不能直接操作堆(主内存)的,操作的是其数据的副本,在多线程环境下,两个线程对同一个变量a(初始为0)进行操作时,线程A取到了变量a的初始值为0,然后对其进行操作,但是,在操作完之后,在刷新回内存之前,线程B又从内存中读取了变量a,此时线程B获取到a的值还是为0,此时线程A把计算后的数据写回到内存,再线程B写回到内存时,就会发现线程B把线程A的数据覆盖了,同时也失去了原子性。解决方案:采用volatile来保证数据的可见性,采用加锁或循环CAS保证数据原子性。
Java内存模型中重排序
什么是重排序?
解析过程:源代码 -> 编译器优化重排序(编译期间) -> 指令级并行重排序 -> 内存系统重排序 -> 最终执行指令序列
- 存在数据有依赖性,不会进行重排序
写后读,写后写,都后写
//如果有以下代码
int a = 1;
int b = 2;
int c = a + b;
上面代码可以发现,变量c是依赖于变量a、b,所以c的顺序是固定的,但是a和b互不依赖,所以a和b的执行顺序并不是按照编写的顺序来进行的,也有可能先执行b再执行a。但是不会对c进行调整,因为存在数据依赖性。
- 存在控制依赖性,不会进行重排序
int a = 2;
int b = 3;
int c = 0;
boolean flag = true;
if(flag){
c = a*b;
}
上面代码可以看到,a、b、c、可能发生重排序。但是在if和c=a*b是不会发生重排序的。但是在操作系统中,也会出现猜测执行,即可能会先执行a*b,将结果存放到重排序缓存中,当flag为true的时候,就可以直接将缓存中的值拿出来进行赋值,但是也会出现猜测执行时,a和b还没有进行赋值,所以也有可能会猜测失败,猜测失败就会重新进行计算。
禁止重排序 - 内存屏障(强制刷出cache)
在编译期间,会在生成指令序列的时候,会在一些禁止重排序的地方,插入内存屏障从而禁止操作系统对指令进行重排序。
volatile关键字就是通过该方式来保证数据的可见性
- Load1;LoadLoad;Load2 确保Load1的数据装载,是在Load2及之后所有指令之前进行装载,即Load1装载完毕之后,后面的才能够装载。
- Store1;StoreStore;Store2 确保Store1资源已经刷新到内存,即对其他处理器可见时,才会去执行Store2或之后的指令。
- Load1;LoadStore;Store2 确保Load1的数据装载之后才会执行Store2。
- Store1;StoreLoad;Load1 确保Store1刷新回内存之后,才会去执行Load1(x86只提供该指令)。
临界区
临界区表示在synchronized代码块,即进入同步块和退出同步块。
在临界区内的代码会发生重排序。
Happens - Befores
两个操作之间具有happens-before关系,并不意味着前一个操作必须要在后一个操作之前执行!
happens-before仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第 二个操作之前。
- 前者表示,对于Java程序员来说,一个操作happens-before另一个操作,那么第一个操作的执行结果将对第二个操作可见,而且第一个操作的执行顺序排在第二个操作之前。(就是说前一个指令必须在下一个指令之前完成,至少在执行下一个指令之前,能看到上一个指令执行的结果)
- 后者表示,对于编译器和处理器来说,两个操作之间存在happens-before关系,并不意味着Java平台的具体实现必须要按照happens-before关系指定的顺序来执行。如果重排序之后的执行结果,与按happens-before关系来执行的结果一致,那么这种重排序是允许的。(就上面的代码来说,c依赖于a、b,操作系统完全可以把ab的顺序发生更改,c不会感知到其顺序的改变,只关注结果,不关注过程)
int a = 1;
int b = 2;
int c = a*b;
从上面代码看到:
- ① a happens-befores b
- ② b happens-befores c
- ③ 由①和②推算出 a happens-befores c
由此可见,②和③的执行,如果发生了变化,那么执行结果将不是预期的,所以JMM会禁止编译器和处理器对其进行重排序,但是①和②进行重排序是不会对③的执行结果改变,所以是允许对①和②进行重排序
转载:https://blog.csdn.net/weixin_42666318/article/details/106095714