文章目录
前言
JVM嘛,Java虚拟机,也就是一个虚拟的硬件机器,所以它的一切功能都是基于真实的硬件机器,可能做了一些灵活的应用.
所以学习JVM的内存模型前,需要先了解下计算机硬件是如何解决并发带来的数据一致性和乱序执行问题.
JVM的内存模型和计算机的内存模型是有一定的映射关系的,但很多定义并不相同.
内存模型
读取速度:CPU 远大于 内存 远大于 磁盘
这个远大于大概就是100倍+
存储器的层次结构
这个金字塔,从最上层L0开始,越往下 速度越慢,成本越低,空间越大
L0 L1 L2是CPU内部的缓存,每个CPU或者CPU的核心独享
数据一致性
上面看到L0 L1 L2是CPU内部的缓存
当两个CPU从L4或L3读取数据到自己的L2后,假如其中一个CPU修改了数据,就可能产生数据不一致的问题.
当然硬件的发明者不会这么傻,肯定有解决方案,那硬件是怎么保证数据一致性的呢?
总线锁
CPU去读主存或者其他层级的时候,是通过内存总线读的.
以前的CPU,为了保证数据一致性,会这么干:
CPU1访问变量t的时候,会锁住总线,这时候CPU2就不能访问变量t了,直到CPU1释放锁.
总线锁会锁住总线,使得其他CPU甚至不能访问内存中其他的地址,因而效率较低.
缓存锁+总线锁
各种缓存一致性协议
最常听的是intel的CPU的MESI协议,参考这个:https://www.cnblogs.com/z00377750/p/9180644.html
cache line 缓存行
cache line是读取缓存的基本单位
CPU去L3读取一个变量t时,假如t的数据很小,那么并不只是读取t自己,而是会带着t身边的一块数据,这个块成为"基本缓存单位",即cache line缓存行,大小一般是64Bits
伪共享问题
假如CPU1需要读写变量t1,CPU2需要读写变量t2,而t1和t2在同一个缓存行中,这俩CPU就会产生不必要的互相影响,不必要的重新读取变量
一些优秀的开源软件,代码中会规避掉缓存行的伪共享问题,比如Disruptor的cursor,就是前后各加了7个long变量,保证cursor变量一定是在独立的缓存行中,即缓存行对齐
下面我们来一对例子,验证一下伪共享:
反例:
static void originalMethod() throws InterruptedException {
long[] arr = new long[2];
Thread t1 = new Thread(() -> {
for (int i = 0; i < TIMES; i++) {
arr[0] = i;
}
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < TIMES; i++) {
arr[1] = i;
}
});
long start = System.currentTimeMillis();
t1.start();
t2.start();
t1.join();
t2.join();
long end = System.currentTimeMillis();
System.out.println("originalMethod :" + (end - start));
}
正例,就是多占一些内存,把使用的东西分成不同的缓存行:
这个实验结果,优化后快很多
// 这个类,就是只用到了num,其他的参数用来填充缓存行
static class Param {
long l1, l2, l3, l4, l5, l6, l7;
long num;
}
static void optimizedMethod() throws InterruptedException {
Param[] arr = {
new Param(), new Param()};
Thread t1 = new Thread((