学习来自马士兵
什么是内存局部性原理?
见https://blog.csdn.net/xindoo/article/details/97525694 可以看下内存局部性这块的解释
先上代码
package com.example.mashibing;
import java.util.concurrent.CountDownLatch;
public class Demo1 {
static CountDownLatch countDownLatch = new CountDownLatch(2);
// 内存空间局部性原理 优化
public static class T {
// public long p1611 = 1;
// public long p161 = 1;
// public long p151 = 1;
// public long p141 = 1;
// public long p131 = 1;
// public long p121 = 1;
// public long p111 = 1;
public volatile long p1 = 1;
}
public static void main(String[] args) throws InterruptedException {
long l = System.currentTimeMillis();
T t1 = new T();
T t3 = new T();
Thread t = new Thread(() -> {
for (int i = 0; i < 1_0000_0000; i++) {
t1.p1 ++;
}
countDownLatch.countDown();
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1_0000_0000; i++) {
t3.p1 ++;
}
countDownLatch.countDown();
});
t.start();
t2.start();
countDownLatch.await();
long l1 = System.currentTimeMillis();
System.out.println(l1 - l);
}
}
下面列举5次运行时间:
- 3450
- 3886
- 3969
- 3446
- 3789
将上面代码注释打开
package com.example.mashibing;
import java.util.concurrent.CountDownLatch;
public class Demo1 {
static CountDownLatch countDownLatch = new CountDownLatch(2);
// 内存空间局部性原理 优化
public static class T {
public long p1611 = 1;
public long p161 = 1;
public long p151 = 1;
public long p141 = 1;
public long p131 = 1;
public long p121 = 1;
public long p111 = 1;
public volatile long p1 = 1;
}
public static void main(String[] args) throws InterruptedException {
long l = System.currentTimeMillis();
T t1 = new T();
T t3 = new T();
Thread t = new Thread(() -> {
for (int i = 0; i < 1_0000_0000; i++) {
t1.p1 ++;
}
countDownLatch.countDown();
});
Thread t2 = new Thread(() -> {
for (int i = 0; i < 1_0000_0000; i++) {
t3.p1 ++;
}
countDownLatch.countDown();
});
t.start();
t2.start();
countDownLatch.await();
long l1 = System.currentTimeMillis();
System.out.println(l1 - l);
}
}
下面列举5次运行时间:
- 760
- 761
- 781
- 786
- 766
why
我们知道程序所需的数据运行时需要加载到内存中去,如下图,程序中的一个字段i为int类型,(这里以Java为例)我们知道一个int类型占4个字节,我们程序需要去对i进行一个加一操作时会做什么那?(这里我们说单线程情况)
- 从主存load i到L3
- 从L3load i到L2
- 从L2 load i到L1
- L1 iload到寄存器
- 运算器对i进行加一操作后写回到L1 L2 L3 主存
- 主存i =1
过于上述过程提2个问题
- i占4个字节,从主存读取i时只读4个字节吗?
- 多线程访问i时,如何解决并发(次要)
关于第一个问题 答案必然是否,那他一次读取多少个字节那?
看下这块https://lrita.github.io/2018/06/30/programmer-should-know-about-memory-1/#MathJax-Element-1-Frame 有兴趣的小伙伴可以看下全篇 很赞!不跑题,我们可以这里记一下一般是一次读取64个字节
第二个问题,这个不同的语言都有自己的解决方案,如这里我使用的Java的,我通过volatile来保障i字段具有内存可见性,不过这里由于i++
不是原子操作,不能保障每次的修改都能同步到主存,不过在1E次的修改中必然是会有同步的操作的
现在来解答为什么最初的两个程序为什么效率相差很大
- 两个线程同时new一个对象时,他们均需要在主存申请内存空间,根据内存行读取一次大小为64字节和内存空间局部性原理,我们可以推测如果这个对象占用大小小于64字节,就会存在这样的情况,如下图,
- 即T1和T2的T对象申请内存时
挨得很近
由于自身占用内存过小,加到一块都不到64字节,这样程序运行时就会发生T1和T2进行i运算时读取的是同一块64byte的内存行
到L3 L2 L1 通用寄存区又由于我加了volatile关键字,保障了内存可见性,导致当i被修改后会通知到数据总线导致该数据会被写回到主存,另一个线程同这种情况 - 这样子假设当第一次主存的i被T1修改后写会到主存,T2线程就会感知到同时从主存去拉取T1线程修改过的数据,而当T2同步后他要去修改数据并修改成功后也会同步到主存同时通知到T1,这样子就多了很多T1、T2与主存交互的操作,因此第一次的代码非常耗时
- 第二次代码加了7个long类型的字段,我们知道long类型占8个字节,加上那个volatile的字段,一共就有64个字节,这样子一个对象申请的内存空间一定是大于64字节的,就能够避免两个线程的T对象申请的内存会作为一个
64字节
的内存行被CPU去读取了,因为每一个T对象自身已经大于了64字节