本文参考自b站视频,学习总结 https://www.bilibili.com/video/BV1DK411p7cy
多核并发缓存架构
JMM内存模型
Java线程内存模型跟cpu缓存模型类似,是基于cpu缓存模型来建立的,Java线程内存模型是标准化的,屏蔽掉了底层不同计算机的区别。
JMM数据原子操作
- read(读取):从主内存读取数据
- load(载入):将主内存读取到的数据写入工作内存
- use(使用):从工作内存读取数据来计算
- assign(复制):将计算好的值重新复制到工作内存中
- store(存储):将工作内存数据写入主内存
- write(写入):将store过去的变量复制给主内存中的变量
- lock(锁定):将主内存变量加锁,标志位线程独占状态(锁的是store和write操作)
- unlock(解锁):将主内存变量解锁,解锁后其他线程可以锁定该变量
JMM缓存不一致问题解决方案
-
总线加锁(性能太低)
cpu从主内存读取数据到高速缓存,会在总线对这个数据加锁,这样其他cpu没法去读或写这个数据,直到这个cpu使用完数据释放锁之后其他cpu才能读取该数据
-
MESI缓存一致性协议
多个cpu从主内存读取同一个个数据到各自的告诉缓存,当其中某个cpu修改了缓存里的数据,该数据会立马同步回主内存,其他cpu通过总线嗅探机制可以感知到数据的变化从而将自己缓存里的数据失效
Volatile可见性底层实现原理
JMM内存交互层面:volatile修饰的变量read、load、use操作和assign、store、write必须是连续的,即修改后必须立即同步会主内存,使用时必须从主内存刷新,由此保证volatile变量的可见性。
底层实现主要是通过汇编lock前缀指令,它会锁定这块区域的缓存(缓存行锁定)并回写到主内存
IA-32架构软件开发手册对lock指令的解释:
- 会将当前处理器缓存行的数据立即写回系统内存
- 这个会写内存的操作会引起在其他CPU里缓存了该内存地址的数据无效(MESI)协议
Volatile可见性、原子性、有序性
- 线程可见性测试
public class VolatileVisibilityTest {
private static boolean initFlag = true;//不使用volatile修饰
//private static boolean initFlag = true;//使用volatile修饰
public static void main(String[] args) throws InterruptedException {
new Thread(new Runnable() {
@Override
public void run() {
System.out.println("waiting data...");
while (!initFlag) {
}
System.out.println("================success=========");
}
}).start();
Thread.sleep(2000);
new Thread(new Runnable() {
@Override
public void run() {
prepareData();
}
}).start();
}
public static void prepareData() {
System.out.println("prepareing data...");
initFlag = true;
}
}
- 原子性测试(不能保证原子性)
public class VolatileAtomicTest {
private static int counter = 0;//不使用volatile修饰
//private static volatile int counter = 0;//使用volatile修饰
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
Thread thread = new Thread(() ->{
for (int j = 0; j < 1000; j++) {
counter++;
}
});
thread.start();
}
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println(counter);
}
}
- 有序性测试
public class VolatileReOrderSample {
private static int x = 0, y = 0;//不使用volatile修饰
private static int a = 0, b = 0;//不使用volatile修饰
//private static volatile int x = 0, y = 0;//使用volatile修饰
//private static volatile int a = 0, b = 0;//使用volatile修饰
public static void main(String[] args) throws InterruptedException {
int i = 0;
for (; ; ) {
i++;
x = 0;
y = 0;
a = 0;
b = 0;
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
shortWait(10000);
a = 1;
x = b;
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
b = 1;
y = a;
}
});
t1.start();
t2.start();
t1.join();
t2.join();
String result = "第" + i + "次(" + x + "," + y + ")";
if (x == 0 && y == 0) {
System.out.println(result);
break;
} else {
System.out.println(result);
}
}
}
/**
* 等待一段时间,时间单位纳秒
* @param interval
*/
public static void shortWait(long interval) {
long start = System.nanoTime();
long end;
do {
end = System.nanoTime();
Random r = new Random(1);
} while (start + interval >= end);
}
}
解释:这里有两个线程,我们不断地循环,每一轮循环x y a b都会被重置为0,然后每个值都会在两个线程中进行修改。假设不会重排序,程序按照从上到下代码编写顺序执行,那么理论上可能出现的结果值:
- 线程1先启动执行,线程2后:a=1,x=b=0,b=1,y=a=1(abxy =1,1,0,1)
- 线程2先启动执行,线程1后:b=1,y=a=0,a=1,x=b=1(abxy=1,1,1,0)
- 线程1或者2先启动,两个都没有执行完;比如线程1执行完a=1后被阻塞,线程2马上开始执行,执行到b=1阻塞,然后任意一个开始执行最后结果有可能出现x=1 并且y=1;那么结果可能出现abxy=1111;
所以理论上代码是从上到下按序执行那么 x == 0 && y == 0
的情况不会出现;
那么真的不会出现吗?可以不使用volatile修饰执行。
结果是程序会停止执行
cpu为了执行效率,会对指令重拍序;有可能出现:线程1中x=b;拍到a=1;前边,线程2中y = a;拍到b=1;前边,这种情况下就有可能出现x=0,y=0
//如果会重排序,代码可能出现这种情况
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
shortWait(10000);
x = b;
a = 1;
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
y = a;
b = 1;
}
});
所以本例中,用volatile修饰abxy后,如果程序不会停止,一致运行,就说明不会重排序。最终结果也是不会停止,结论就是volatile可以防止指令重排序。