并发编程01-JMM&volatile

参考文档:一文解决内存屏障 https://www.jianshu.com/p/64240319ed60

现代计算机理论模型与工作原理

冯诺依曼计算机模型

简介:

​ 现代计算机模型是基于-冯诺依曼计算机模型 计算机在运行时,先从内存中取出第一条指令,通过控制器的译码,按指令的要求,从存 储器中取出数据进行指定的运算和逻辑操作等加工,然后再按地址把结果送到内存中去。接下来,再取出第二条指令,在控制器的指挥下完成规定操作。依此进行下去,直至遇到停止指令。

计算机五大核心组成部分

  • 控制器
  • 运算器
  • 存储器
  • 输入
  • 输出

冯诺依曼计算机模型图

image-20210829224323237

现代计算机硬件结构原理图

image-20210829224618735

CPU多核缓存架构

CPU内部结构划分

  • 控制单元(包含指令计数器、指令寄存器等)
  • 运算单元
  • 存储单元

多CPU

​ 一个现代计算机通常由两个或者多个CPU,如果要运行多个程序(进程)的话,假如只有 一个CPU的话,就意味着要经常进行进程上下文切换,因为单CPU即便是多核的,也只是多个处理器核心,其他设备都是共用的,所以多个进程就必然要经常进行进程上下文切换,这个代价是很高

CPU多核支持进程内同时跑多个线程

​ 一个现代CPU除了处理器核心之外还包括寄存器、L1L2L3缓存这些存储设备、浮点运算 单元、整数运算单元等一些辅助运算设备以及内部总线等。一个多核的CPU也就是一个CPU上 有多个处理器核心,这样有什么好处呢?比如说现在我们要在一台计算机上跑一个多线程的程序,因为是一个进程里的线程,所以需要一些共享一些存储变量,如果这台计算机都是单核单线程CPU的话,就意味着这个程序的不同线程需要经常在CPU之间的外部总线上通信同时还要处理不同CPU之间不同缓存导致数据不一致的问题,所以在这种场景下多核单CPU的架构就能发挥很大的优势,通信都在内部总线,共用同一个缓存。

CPU寄存器

​ CPU在寄存器上执行操作的 速度远大于在主存上执行的速度。这是因为CPU访问寄存器的速度远大于主存。大概是几十到几百倍

CPU缓存

​ 即高速缓冲存储器,是位于CPU与主内存间的一种容量较小但速度很高的存储器。由于 CPU的速度远高于主内存,CPU直接从内存中存取数据要等待一定时间周期,Cache中保存着CPU刚用过或循环使用的一部分数据,当CPU再次使用该部分数据时可从Cache中直接调用, 减少CPU的等待时间,提高了系统的效率。 一级Cache(L1 Cache) 二级Cache(L2 Cache) 三级Cache(L3 Cache),运行速度 寄存器>L1>L2>L3>内存

内存

​ 一个计算机还包含一个主存。所有的CPU都可以访问主存。主存通常比CPU中的缓存大得 多

缓存一致性协议(MESI)

MESI协议的作用

多线程环境下存在的问题:缓存一致性问题

​ 在多处理器系统中,每个处理器都有自己的高速缓存,而它们又共享同一主内存。基于高速缓存的存储交互很好地解决了处理器与内存的速度矛盾,但是 也引入了新的问题:缓存一致性(CacheCoherence)。当多个处理器的运算任务都涉及同一 块主内存区域时,将可能导致各自的缓存数据不一致的情况,如果真的发生这种情况,那同步回到主内存时以谁的缓存数据为准呢?

​ 为了解决一致性的问题,需要各个处理器访问缓存时都 遵循一些协议,在读写时要根据协议来进行操作,这类协议有MSI、 MESI(IllinoisProtocol)、MOSI、Synapse、Firefly及DragonProtocol,等等

MESI缓存一致性协议

状态 描述 监听任务
M 修改 (Modified) 该Cache line有效,数据被修改了,和内存中的数据不一致,数据只存在于本Cache中。 缓存行必须时刻监听所有试图读该缓存行相对就主存的操作,这种操作必须在缓存将该缓存行写回主存并将状态变成S(共享)状态之前被延迟执行。
E 独享、互斥 (Exclusive) 该Cache line有效,数据和内存中的数据一致,数据只存在于本Cache中。 缓存行也必须监听其它缓存读主存中该缓存行的操作,一旦有这种操作,该缓存行需要变成S(共享)状态。
S 共享 (Shared) 该Cache line有效,数据和内存中的数据一致,数据存在于很多Cache中。 缓存行也必须监听其它缓存使该缓存行无效或者独享该缓存行的请求,并将该缓存行变成无效(Invalid)。
I 无效 (Invalid) 该Cache line无效。

缓存行:CPU缓存的最小存储单元

总线嗅探机制:多个cpu读取同一份内存变量A到cpu缓存后会嗅探别的cpu对该变量A的读取而修改状态

指令周期内会进行裁决:多个cpu同时向总线递交modified时,指令周期内会裁决A,B哪个线程生效,如果A生效,另一个线程B 标记invalid且已执行过程序(类似代码跑完了,对这个线程来说却实际没有生效)

缓存一致性协议失效的情况:

1、要缓存的数据大于一个缓存行的大小,只能加总线锁(一个缓存行可以存多个数据)

2、CPU本身不支持缓存一致性协议

什么是线程

什么是进程

​ 现代操作系统在运行一个程序时,会为其创建一个进程。例如,启动一个Java程序,操作系统就会创建一个Java进程。进程是系统分配资源的基本单位

线程

现代操作系统调度CPU的最小单元是线程,也叫轻量级进程 (Light Weight Process),在一个进程里可以创建多个线程,这些线程都拥有各自的计数器、堆栈和局部变量等属性,并且能够访问共享的内存变量。处理器在这些线程上高速切换, 让使用者感觉到这些线程在同时执行。

线程分为两类

  • 用户线程(User-Level Thread)ULT-对应用户空间
    • 指不需要内核支持而在用户程序中实现的线程,其不依赖于操作系统核心,应用进程利用线程库提供创建、同步、调度和管理线程的函数来控制用户线程。另外,用户线程 是由应用进程利用线程库创建和管理,不依赖于操作系统核心。不需要用户态/核心态切换, 速度快。操作系统内核不知道多线程的存在,因此一个线程阻塞将使得整个进程(包括它的所 有线程)阻塞。由于这里的处理器时间片分配是以进程为基本单位,所以每个线程执行的时间相对减少。
  • 内核线程(Kernel-Level Tread) KLT-对应内核空间
    • 线程的所有管理操作都是由操作系统内核完成的。内核保存线程的状态和上下 文信息,当一个线程执行了引起阻塞的系统调用时,内核可以调度该进程的其他线程执行。在 多处理器系统上,内核可以分派属于同一进程的多个线程在多个处理器上运行,提高进程执行的并行度。由于需要内核完成线程的创建、调度和管理,所以和用户级线程相比这些操作要慢 得多,但是仍然比进程的创建和管理操作要快。大多数市场上的操作系统,如Windows, Linux等都支持内核级线程

在 4 GB 的内存空间中,只有 3 GB 可以用于用户应用程序。一个进程只能运行 在用户方式(usermode)或内核方式(kernelmode)下。用户程序运行在用户方式下,而系统调用运行在内核方式下。在这两种方式下所用的堆栈不一样:用户方式下用的是一般的堆栈,而内核方式下用的是固定大小的堆栈(一般为一个内存页的大小) 每个进程都有自己的 3 G 用户空间,它们共享1GB的内核空间。当一个进程从用户空间进 入内核空间时,它就不再有自己的进程空间了。这也就是为什么我们经常说线程上下文切换会 涉及到用户态到内核态的切换原因所在

Java线程与内核线程的关系

image-20210830151225845

Java线程生命状态

  • 新建
  • 就绪
  • 运行
  • 终止
  • 阻塞
  • 等待
  • 超时等待

image-20210830152624403

图片

并发(多线程)的意义

并发的作用

1、充分利用多核CPU的计算能力

​ 在单cpu系统中,每一时间点只能有一道程序执行,即微观上这些程序是分时的交替执行,因为分时交替运行的时间是非常短的,只不过给人的感觉是同时运行,在宏观上并发和并行一样都是同时在进行但是在微观上是不一样的。在多个cpu的操作系统中,这些可以并发执行的程序便可以分配到多个处理器(cpu),实现多任务并行执行,就是利用每个处理器来处理一个可以并发执行的程序,这样多个程序便可以同时执行。核越多,并行处理的程序越多,可以大大提高电脑的运行效率。如果能够合理地使用多线程,将能够缩减复杂应用程序的开发和维护成本,并能提供更好的性能。通过将异步工作流转换为多个序列化工作流,多线程可以更好地对人类的工作和交互方式建模。使用多线程,很多复杂的代码将变得更加直截了当,因此更容易编写、阅读和维护,但是使用多线程也意味着有很大的风险

2、方便进行业务拆分,提升应用性能

并发产生的问题

1、产生频繁的上下文切换

2、多线程安全问题,容器出现死锁,产生的死锁会造成系统功能的阻塞或者不可用

3、代码复杂度提升。。。

JMM模型

JMM概念(Java Memory Model)

JMM与JVM内存区域划分是不同的概念层次,JMM描述的是一组规则,通过这种规则控制程序中各个变量在工作内存和主内存的访问方式,JMM是围绕着原子性有序性可见性展开的。

image-20210830175453639

JMM与硬件内存架构的关系

JMM模型是依据现代计算机理论模型,以CPU多核缓存架构(硬件内存架构)为基础,屏蔽底层操作系统实现而抽象出来的一种抽象模型

image-20210830175639511

JMM内存交互操作

  1. lock(锁定):作用于主内存的变量,把一个变量标记为一条线程独占状态
  2. unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
  3. read(读取):作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
  4. load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中
  5. use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎
  6. assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量
  7. store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write的操作
  8. write(写入):作用于主内存中的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中

现代计算机主要结构组成

​ 把一个变量从主内存中复制到工作内存中,就需要按顺序地执行read和load操作,如果把变量从工作内存中同步到主内存中,就需要按顺序地执行store和write操作。但Java内存模型只要求上述8大操作(原子操作)必须按顺序执行,而没有保证必须是连续执行。

image-20210830181231664

JMM内存同步规则

  1. 不允许一个线程无原因地(没有发生过任何assign操作)把数据从工作内存同步回主内存中
  2. 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或者assign)的变量。即就是对一个变量实施use和store操作之前,必须先自行assign和load操作。
  3. 一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,变量才会被解锁。lock和unlock必须成对出现
  4. 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量之前需要重新执行load或assign操作初始化变量的值。
  5. 如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去unlock一个被其他线程锁定的变量。
  6. 对一个变量执行unlock操作之前,必须先把此变量同步到主内存中(执行store和write操作)

volatile原理与内存语义

volatile是Java虚拟机提供的轻量级的同步机制

volatile语义有如下两个作用
可见性:保证被volatile修饰的共享变量对所有线程总数可见的,也就是当一个线程修改了一个被volatile修饰共享变量的值,新值总是可以被其他线程立即得知。

有序性:禁止指令重排序优化(通过内存屏障)。

volatile缓存可见性实现原理

​ JMM内存交互层面:volatile修饰的变量的read、load、use操作和assign、store、write必须是连续的,即修改后必须立即同步会主内存,使用时必须从主内存刷新,由此保证volatile变量的可见性。

​ 底层实现:通过汇编lock前缀指令,它会锁定变量缓存行区域并写回主内存,这个操作称为“缓存锁定”,缓存一致性机制会阻止同时修改被两个以上处理器缓存的内存区域数据。一个处理器的缓存回写到内存内存会导致其他处理器的缓存无效

​ 汇编代码查看
-XX:+UnlockDiagnosticVMOptions -XX:+PrintAssembly -Xcomp

可见性&原子性&有序性

并发编程三大特性

  • 可见性:当一个线程修改了某个共享变量的值,其他线程是否能够马上得知这个修改的值(高速缓存操作和指令重排会导致可见性丢失)

  • 原子性:原子性指的是一个操作是不可中断的,即使是在多线程环境下,一个操作一旦开始就不会

    被其他线程影响。

  • 有序性:Java允许编译器和处理器对指令进行重排序,但是重排序过程不会影响到单线程程序的执行,却会影响到多线程并发执行的正确性。

​ volatile保证可见性与有序性,但是不能保证原子性,要保证原子性需要借助synchronized、Lock锁机制,同理也能保证有序性与可见性。因为synchronized和Lock能够保证任一时刻只有一个线程访问该代码块。

有序性&指令重排

​ java语言规范规定JVM线程内部维持顺序化语义。单线程内保证串行语义执行的一致性:即只要程序的最终结果与它顺序化情况的结果相等,那么指令的执行顺序可以与代码顺序不一致,此过程叫指令的重排序
​ 指令重排序的意义:JVM能根据处理器特性(CPU多级缓存系统、多核处理器等)适当的对机器指令进行重排序,使机器指令能更符合CPU的执行特性,最大限度的发挥机器性能。可以理解为:只要不影响程序单线程、顺序执行的结果,就可以对两个指令重排序。
编译器CPU处理器中都能执行指令重排优化操作

JMM如何解决原子性&可见性&有序性问题

as-if-serial语义

不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义。
为了遵守as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操作做重排序,因为这种重排序会改变执行结果。但是,如果操作之间不存在数据依赖关系,这些操作就可能被 编译器和处理器重排序。

happens-before 原则

从JDK 5开始,Java使用新的JSR-133内存模型,提供了 happens-before 原则来辅助保证程序执行的原子性、可见性以及有序性的问题,它是判断数据是否存在竞争、线程是否安全的依据,happens-before 原则内容如下

1、程序顺序原则:即在一个线程内必须保证语义串行性,也就是说按照代码顺序执行。

2、锁规则:解锁(unlock)操作必然发生在后续的同一个锁的加锁(lock)之前,也就是说, 如果对于一个锁解锁后,再加锁,那么加锁的动作必须在解锁动作之后(同一个锁)。

3、volatile规则:volatile变量的写,先发生于读,这保证了volatile变量的可见性,简单的理解就是,volatile变量在每次被线程访问时,都强迫从主内存中读该变量的值,而当该变量发生变化时,又会强迫将最新的值刷新到主内存,任何时刻,不同的线程总是能够看到该变量的最新值。

4、线程启动规则:线程的start()方法先于它的每一个动作,即如果线程A在执行线程B的start方法之前修改了共享变量的值,那么当线程B执行start方法时,线程A对共享变量的修改对线程B可见

5、传递性:A先于B ,B先于C 那么A必然先于C

6、线程终止规则:线程的所有操作先于线程的终结,Thread.join()方法的作用是等待当前执行的线程终止。假设在线程B终止之前,修改了共享变量,线程A从线程B的join方法成功返回后,线程B对共享变量的修改将对线程A可见。

7、 线程中断规则:对线程 interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过Thread.interrupted()方法检测线程是否中断。

8、对象终结规则:对象的构造函数执行,结束先于finalize()方法

volatile保证可见性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
public class VolatileVisibilitySample {
volatile boolean initFlag = false;

public void save(){
this.initFlag = true;
String threadname = Thread.currentThread().getName();
System.out.println("线程:"+threadname+":修改共享变量initFlag");
}

public void load(){
String threadname = Thread.currentThread().getName();
while (!initFlag){
System.out.println("线程:"+threadname+"当前线程还在跑空循环");
//线程在此处空跑,等待initFlag状态改变
}
System.out.println("线程:"+threadname+"当前线程嗅探到initFlag的状 态的改变");
}


public static void main(String[] args){
VolatileVisibilitySample sample = new VolatileVisibilitySample();
Thread threadA = new Thread(()->{ sample.save(); },"threadA");
Thread threadB = new Thread(()->{ sample.load(); },"threadB");
threadB.start();
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
threadA.start();
}
}

volatile无法保证原子性

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
/**
* volatile无法保证原子性
*/
public class VolatileAtomicSample {

private static volatile int counter = 0;

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++; //不是一个原子操作,第一轮循环结果是没有刷入主存,这一轮循环已经无效
//1 load counter 到工作内存
//2 add counter 执行自加
//其他的代码段?
}
});
thread.start();
}

try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}

System.out.println(counter);
}

}

volatile禁止重排优化

案例一:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
public class VolatileReOrderSample {
private static int x = 0, y = 0;
private static int a = 0, b =0;

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() {
//由于线程one先启动,下面这句话让它等一等线程two. 读着可根据自己电脑的实际性能适当调整等待时间.
shortWait((long) (Math.random() * 1000));
a = 1; //是读还是写?store,volatile写
//storeload ,读写屏障,不允许volatile写与第二部volatile读发生重排
//手动加内存屏障
//UnsafeInstance.reflectGetUnsafe().storeFence();
x = b; // 读还是写?读写都有,先读volatile,写普通变量
//分两步进行,第一步先volatile读,第二步再普通写
}
});
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
shortWait((long) (Math.random() * 100));
b = 1;
//UnsafeInstance.reflectGetUnsafe().storeFence();
y = a;
}
});
t1.start();
t2.start();
t1.join();
t2.join();

/**
* cpu或者jit对我们的代码进行了指令重排?
* 1,1
* 0,1
* 1,0
* 0,0
*/
String result = "第" + i + "次 (" + x + "," + y + ")";
if(x == 0 && y == 0) {
System.err.println(result);
break;
} else {
System.out.println(result);
}
}

}

public static void shortWait(long interval){
long start = System.nanoTime();
long end;
do{
end = System.nanoTime();
}while(start + interval >= end);
}

}

案例二:DCL 单例模式-双重检查

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
public class DoubleCheckLock {

private static volatile DoubleCheckLock instance;

private DoubleCheckLock() {
}

public static DoubleCheckLock getInstance(){
if (null != instance){
synchronized (DoubleCheckLock.class){
if(null == instance){
instance = new DoubleCheckLock();
}
}
}
return instance;
}
}

因为instance = new DoubleCheckLock();可以分为以下3步完成(伪代码)

memory = allocate();//1.分配对象内存空间

instance(memory);//2.初始化对象

instance = memory;//3.设置instance指向刚分配的内存地址,此时

instance!=null

由于步骤2和步骤3间可能会重排序,如下:

memory=allocate();//1.分配对象内存空间

instance=memory;//3.设置instance指向刚分配的内存地址,此时instance!

=null,但是对象还没有初始化完成!

instance(memory);//2.初始化对象

由于步骤2和步骤3不存在数据依赖关系,而且无论重排前还是重排后程序的执行结果在单

线程中并没有改变,因此这种重排优化是允许的。但是指令重排只会保证串行语义的执行的一

致性(单线程),但并不会关心多线程间的语义一致性。所以当一条线程访问instance不为null

时,由于instance实例未必已初始化完成,也就造成了线程安全问题。那么该如何解决呢,很

简单,我们使用volatile禁止instance变量被执行指令重排优化即可。

内存屏障(Memory Barrier)

内存屏障,又称内存栅栏,是一个CPU指令,它的作用有两个,一是保证特定操作的执行

顺序二是保证某些变量的内存可见性(利用该特性实现volatile的内存可见性)。由于编译

器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译器

和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏

障禁止在内存屏障前后的指令执行重排序优化。Memory Barrier的另外一个作用是强制刷出

各种CPU的缓存数据,因此任何CPU上的线程都能读取到这些数据的最新版本。总之,

volatile变量正是通过内存屏障实现其在内存中的语义,即可见性和禁止重排优化

image-20210831163732484

为了实现volatile的内存语义,编译器在生成字节码时,会在指令序列中插入内存屏障来 禁止特定类型的处理器重排序。对于编译器来说,发现一个最优布置来最小化插入屏障的总数 几乎不可能。为此,JMM采取保守策略。下面是基于保守策略的JMM内存屏障插入策略:

  • 在每个volatile写操作的前面插入一个StoreStore屏障。
  • 在每个volatile写操作的后面插入一个StoreLoad屏障。
  • 在每个volatile读操作的后面插入一个LoadLoad屏障。
  • 在每个volatile读操作的后面插入一个LoadStore屏障

先简单了解两个指令:

Store:将处理器缓存的数据刷新到内存中。

Load:将内存存储的数据拷贝到处理器的缓存中。

JMM定义的内存屏障规范

屏障类型 指令示例 说明
LoadLoad Barriers Load1;LoadLoad;Load2 该屏障确保Load1数据的装载先于Load2及其后所有装载指令的的操作
StoreStore Barriers Store1;StoreStore;Store2 该屏障确保Store1立刻刷新数据到内存(使其对其他处理器可见)的操作先于Store2及其后所有存储指令的操作
LoadStore Barriers Load1;LoadStore;Store2 确保Load1的数据装载先于Store2及其后所有的存储指令刷新数据到内存的操作
StoreLoad Barriers Store1;StoreLoad;Load2 该屏障确保Store1立刻刷新数据到内存的操作先于Load2及其后所有装载装载指令的操作。它会使该屏障之前的所有内存访问指令(存储指令和访问指令)完成之后,才执行该屏障之后的内存访问指令

StoreLoad Barriers同时具备其他三个屏障的效果,因此也称之为全能屏障(mfence),是目前大多数处理器所支持的;但是相对其他屏障,该屏障的开销相对昂贵。

硬件级别和JVM底层对于内存屏障的实现

image-20240403004036768

对于volatile写操作:

  • 在写操作前,通常不需要特别的屏障,因为写屏障(Store Memory Barrier)是放在写操作之后的。
  • 在写操作后,会插入一个写屏障(Store Barrier)。这个写屏障确保了所有在volatile写操作之前的写操作都已经被刷新到主内存,并且后续的读操作能够看到这个volatile写操作的结果。

对于volatile读操作:

  • 在读操作前,会插入一个读屏障(Load Barrier)。这个读屏障确保了在volatile读操作执行时,能够从主内存读取最新的值,防止了从本地缓存或寄存器中读取旧值。
  • 在读操作后,通常不需要特别的屏障,因为读屏障是放在读操作之前的。

volatile关键字与CAS使用过多会产生什么问题?有没有听过总线风暴

1、Cpu工作内存与主内存存在大量交互,且大量的无效工作内存变量产生

嗅探机制

读内存、写内存

无效交互

无效变量产生

导致IO总线通道被大量无效占据,导致总线风暴

2、解决办法:适当使用synchronize关键字