JMM模型(内存模型)、总线锁、缓存锁

本文详细探讨了Java内存模型(JMM)的核心概念,包括缓存一致性问题、内存屏障的作用、volatile关键字的实现原理及其在JMM中的角色。通过分析CPU缓存机制、MESI协议以及内存操作的重排序,揭示了JMM如何解决多线程环境下的可见性、原子性和有序性问题。

这段时间面试对JMM模型有点模糊了,重新写篇文章并整理回忆下

讲JMM,模型之前。先说下基本知识:

CPU内部结构划分

cpu分为:控制单元、运算单元、存储单元
在这里插入图片描述
计算机多硬件多CPU结构:
在这里插入图片描述
计算机核心组件:CPU、内存、I/O设备,三者在处理速度上存在巨大差异,CPU速度最快>内存>I/O设备(磁盘)。
为了提升计算性能,CPU从单核提升到了多核,甚至用到了超线程技术最大化提高CPU处理性能,如果后两者处理性能没有跟上,意味着整体的计算效率取决于最慢的设备。为了平衡三者之间的速度差异,最大化的利用CPU提升性能,从硬件、操作系统、编译器等方面,做出了很多优化:
1、CPU增加高速缓存【硬件层面优化】
2、多核CPU,并且操作系统增加了进程和线程:通过CPU的时间片切换最大化的提升CPU的使用率。【操作系统层面优化】
3、编译器指令优化:更合理的去利用好CPU的高速缓存。【编译器层面优化】
在这里插入图片描述

CPU高速缓存

线程是 CPU 调度的最小单元,线程设计的目是更充分的利用计算机处理的效能,但是大部分运算任务不能只依靠CPU“计算”就能完成,CPU还需要与Memory交互,比如读取运算数据、存储运算结果,这个 I/O 操 作是很难消除的。而由于计算机的存储设备与处理器的运算速度差距非常大,所以现代计算机系统都会增加一层读写速度尽可能接近处理器运算速度的高速缓存来作为内存 和处理器之间的缓冲:将运算需要使用的数据复制到缓存中,让运算能快速进行,当运算结束后再从CPU缓存同步到内存之中。

在这里插入图片描述

高速缓存很好的解决了处理器与内存的速度矛盾,但是也为计算机系统带来了更高的复杂度,因为
它引入了一个新的缓存一致性问题。

什么是缓存一致性问题?
每个 CPU 的处理过程是, 先将计算需要用到的数据缓存在 CPU 高速缓存中,在 CPU 进行计算时,直接从高速缓存中读取数据并且在计算完成之后写入到缓存中。在整个运算过程完成后,再把缓存中 的数据同步到主内存。
由于在多 CPU 种,每个线程可能会运行在不同的 CPU 内, 并且每个线程拥有自己的高速缓存。同一份数据可能会被缓存到多个 CPU 中,如果在不同 CPU 中运行的不同线程看到同一份内存的缓存值不一样就会存在缓存不一致的问题。
为了解决缓存不一致的问题,在 CPU 层面做了很多事情, 主要提供了两种解决办法:
1、总线锁
在多 cpu 下,当其中一个处理器要对共享内存进行操作的时候,在总线上发出一个 LOCK# 信号,这个信号使得其他处理器无法通过总线来访问到共享内存中的数据,总线锁定把 CPU 和内存之间的通信锁住了,这使得锁定期间,其他处理器不能操作其他内存地址的数据,总线锁定的开销比较大,这种机制显然是不合适的。

总线锁的力度太大了,最好的方法就是控制锁的保护粒度,只需要保证对于被多个 CPU 缓存的同一份数据是一致的就可以了。所以引入了缓存锁。

2、缓存锁
相比总线锁,缓存锁即降低了锁的力度。核心机制是基于缓存一致性协议来实现的。
为了达到数据访问的一致,需要各个处理器在访问缓存时遵循一些协议,在读写时根据协议来操作,常见的协议有 MSI、MESI、MOSI 等。最常见的就是 MESI 协议:
MESI 表示缓存行的四种状态,分别是:
1、M(Modify) 表示共享数据只缓存在当前 CPU 缓存中, 并且是被修改状态,也就是缓存的数据和主内存中的数据不一致。
2、E(Exclusive) 表示缓存的独占状态,数据只缓存在当前 CPU 缓存中,并且没有被修改。
3、S(Shared) 表示数据可能被多个 CPU 缓存,并且各个缓存中的数据和主内存数据一致。
4、I(Invalid) 表示缓存已经失效。
在 MESI 协议中,每个缓存的缓存控制器不仅知道自己的 读写操作,而且也监听(snoop)其它 Cache 的读写操作。
对于 MESI 协议,从 CPU 读写角度来说会遵循以下原则:

1、CPU 读请求:缓存处于 M、E、S 状态都可以被读取,I 状 态 CPU 只能从主存中读取数据。
2、CPU 写请求:缓存处于 M、E 状态才可以被写。对于 S 状 态的写,需要将其他 CPU 中缓存行置为无效才可写 使用总线锁和缓存锁机制之后,CPU 对于内存的操作大概 可以抽象成下面这样的结构。从而达到缓存一致性效果。

在这里插入图片描述
MESI 优化带来的可见性问题:
CPU 缓存行的状态是通过消息传递来进行的,如果 CPU0 要对一个在缓存中共享的变量进行写入,首先发送一个失效的消息给到其他缓存了该数据的 CPU。并且要等到他们的确认回执。CPU0 在这段时间内都会处于阻塞状态。
在这里插入图片描述
为了避免阻塞带来的资源浪费。在 cpu 中引入 了 Store Bufferes(存储缓存) 和 Invalidate Queue(无效队列)。
CPU0 写入共享数据时,直接把数据写入到 store bufferes 中,同时发送 invalidate 消息,然后继续去处理其他指令。
当收到其他所有 CPU 发送了 invalidate ACK消息时,再将 store bufferes 中的数据数据存储至 cache 中。最后再从本地Cache同步到主内存。
在这里插入图片描述

但是 cpu 中引入 Store Bufferes 优化存在两个问题:

1、第⑥、⑦步骤中,由于Invalidate消息进入队列后就给CPU-0返回了响应,不能保证第⑦步骤一定完成。
2、引入了 Store Bufferes 后,处理器会先尝试从 Store Bufferes 中读取值,如果 Store Bufferes 中有数据,则直接从Store Bufferes 中读取,否则就再从本地Cache中读取,从Store Bufferes读取数据存在脏读(假设后面cpu1进行修改了a变量,当没有执行刷新失效,即其他cpu进行修改完成的时候,则cpu0的缓存数据与buffer数据会出现不一致,cpu0读取buffer数据)。这个问题目前

CPU 层面的内存屏障

定义:内存屏障就是将 Store Bufferes 中的指令写入到内存,从而使得其他访问同一共享内存的线程的可见性。(将指令写到内存,保证了指令执行的顺序性以及可见性,不同线程对于执行不同命令则保持了统一)

Store Memory Barrier(a.k.a. ST, SMB, smp_wmb)是一条告诉处理器在执行这之后的指令之前,应用所有已经在存储缓存(store buffer)中的保存的指令。这个操作保证了存储的指令被执行了。

Load Memory Barrier (a.k.a. LD, RMB, smp_rmb)是一条告诉处理器在执行任何的加载前,先应用所有已经在失效队列中的失效操作的指令。这个操作保证指令不会存在重复操作

public class StoreBufferesQuestion {

    private int  numone = 0;
    private Boolean  flag = false;

    public void update() {
        numone = 8;
        flag = true;        
    }

    public void test() {
        while (flag) {
            // numone 是多少?
            System.out.println(numone);
        }
    }
}

StoreBufferesQuestion运行结果可能是 0 or 8 。因为读取数据可能是直接从 Store Bufferes 中读取,而这个时候还未执行刷新失效,存在脏读。
通过内存屏障后StoreBufferesQuestion改成如下:

public class StoreBufferesQuestion {

    private int  numone = 0;
    private Boolean  flag = false;

    public void update() {
        numone = 8;
        Store Memory Barrier指令;刷新Store Bufferes
        flag = true;        
    }

    public void test() {
        while (flag) {
            // numone 是多少?
            Load Memory Barrier指令;执行失效消息,保证读之前数据进行过刷新
            System.out.println(numone);
        }
    }
}

这样CPU0在执行读或者写内存的时候就有了屏障进行了保证,顺便说一句volatile关键字就是读前插读屏障,写后加写屏障,避免CPU重排导致的问题,实现多线程之间数据的可见性。
补:

volatile实现原理

那么Volatile是如何来保证可见性的呢?在x86处理器下通过工具获取JIT编译器生成的汇编指令来看看对Volatile进行写操作CPU会做什么事情。
汇编命令

有volatile变量修饰的共享变量进行写操作的时候会多第二行汇编代码,通过查IA-32架构软件开发者手册可知,lock前缀的指令在多核处理器下会引发了两件事情。
1、将当前处理器缓存行的数据会写回到系统内存。
2、这个写回内存的操作会引起在其他CPU里缓存了该内存地址的数据无效。

处理器为了提高处理速度,不直接和内存进行通讯,而是先将系统内存的数据读到内部缓存(L1,L2或其他)后再进行操作,但操作完之后不知道何时会写到内存,如果对声明了Volatile变量进行写操作,JVM就会向处理器发送一条Lock前缀的指令,将这个变量所在缓存行的数据写回到系统内存。但是就算写回到内存,如果其他处理器缓存的值还是旧的,再执行计算操作就会有问题,所以在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每个处理器通过嗅探在总线上传播的数据来检查自己缓存的值是不是过期了,当处理器发现自己缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置成无效状态,当处理器要对这个数据进行修改操作的时候,会强制重新从系统内存里把数据读到处理器缓存里。

Lock前缀指令会引起处理器缓存回写到内存。Lock前缀指令导致在执行指令期间,声言处理器的 LOCK# 信号。在多处理器环境中,LOCK# 信号确保在声言该信号期间,处理器可以独占使用任何共享内存。(因为它会锁住总线,导致其他CPU不能访问总线,不能访问总线就意味着不能访问系统内存),但是在最近的处理器里,LOCK#信号一般不锁总线,而是锁缓存,毕竟锁总线开销比较大。在对于Intel486和Pentium处理器,在锁操作时,总是在总线上声言LOCK#信号。但在P6和最近的处理器中,如果访问的内存区域已经缓存在处理器内部,则不会声言LOCK#信号。相反地,它会锁定这块内存区域的缓存并回写到内存,并使用缓存一致性机制来确保修改的原子性,此操作被称为“缓存锁定”,缓存一致性机制会阻止同时修改被两个以上处理器缓存的内存区域数据。

一个处理器的缓存回写到内存会导致其他处理器的缓存无效。IA-32处理器和Intel 64处理器使用MESI(修改,独占,共享,无效)控制协议去维护内部缓存和其他处理器缓存的一致性。在多核处理器系统中进行操作的时候,IA-32 和Intel 64处理器能嗅探其他处理器访问系统内存和它们的内部缓存。它们使用嗅探技术保证它的内部缓存,系统内存和其他处理器的缓存的数据在总线上保持一致。例如在Pentium和P6 family处理器中,如果通过嗅探一个处理器来检测其他处理器打算写内存地址,而这个地址当前处理共享状态,那么正在嗅探的处理器将无效它的缓存行,在下次访问相同内存地址时,强制执行缓存行填充。

那么内存屏障的操作和细节是什么呢,为什么能保证数据的一致性?请往下看

什么是JMM(Java Memory Model)

由上面的问题,可以知道并发编程导致可见性问题的根本原因是缓存及重排序(比如上一段代码更新和读操作在指令层的执行顺序不一定是和代码一致的)。那么JMM这个概念就提出来了, JMM 实际上就是提供了合理的禁用缓存以及禁止重排序的方法。所以它最核心的价值在于解决可见性和有序性。

在这里插入图片描述
JMM 抽象模型分为主内存、工作内存(本地内存);
主内存:是所有线程 共享的,一般是实例对象、静态字段、数组对象等存储在 堆内存中的变量。
工作内存(本地内存):是每个线程独占的,线程对变量的所有操作都必须在工作内存中进行,不能直接读写主内存中的变量,线程之间的共享变量值的传递都是基于主内存来完成。
JMM 提供了一些禁用缓存以及进制重排序的方法,来解决可见性和有序性问题。这些方法大家都很熟悉: volatile、synchronized、final;

JMM 如何解决顺序一致性问题?
3.3.1、重排序问题
从源代码到最终执行的指令,可能会经过三种重排序:
在这里插入图片描述
编译器重排序,JMM 提供了禁止特定类型的编译器重排序。
处理器重排序,JMM 会要求编译器生成指令时,会插入内存屏障来禁止处理器重排序。

JMM 层面的内存屏障

1、为什么会有内存屏障?
CPU的高速缓存会缓存主存中的数据,缓存的目的就是为了提高性能,避免每次都要向内存取。但是这样的弊端也很明显:不能实时的和内存发生信息交换,分在不同CPU执行的不同线程对同一个变量的缓存值不同。
用volatile关键字修饰变量可以解决上述问题,那么volatile是如何做到这一点的呢?那就是内存屏障。
2、内存屏障是什么?
硬件层的内存屏障分为两种:Load Barrier (读屏障) 和 Store Barrier(写屏障)及 Full Barrier(全屏障) 是读屏障和写屏障的合集。

内存屏障有两个作用:

阻止屏障两侧的指令重排序;
写屏障:强制把写缓冲区/高速缓存中的脏数据等写回主内存,读屏障:将缓冲区/高速缓存中相应的数据失效。

java内存屏障

java的内存屏障通常所谓的四种即LoadLoad(LL),StoreStore(SS),LoadStore(LS),StoreLoad(SL)实际上也是上述两种的组合,完成一系列的屏障和数据同步功能。
LoadLoad(LL)屏障:对于这样的语句Load1; LoadLoad; Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。
StoreStore(SS)屏障:对于这样的语句Store1; StoreStore; Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。
LoadStore(LS)屏障:对于这样的语句Load1; LoadStore; Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。
StoreLoad(SL)屏障:对于这样的语句Store1; StoreLoad; Load2,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。它的开销是四种屏障中最大的。在大多数处理器的实现中,这个屏障是个万能屏障,兼具其它三种内存屏障的功能。

volatile语义中的内存屏障?

volatile的内存屏障策略非常严格保守,非常悲观且毫无安全感的心态:
在每个volatile写操作前插入StoreStore(SS)屏障,在写操作后插入StoreLoad屏障;
在每个volatile读操作前插入LoadLoad(LL)屏障,在读操作后插入LoadStore屏障

由于内存屏障的作用,避免了volatile变量和其它指令重排序、线程之间实现了通信,使得volatile表现出了轻量锁的特性。

final语义中的内存屏障

对于final域,编译器和CPU会遵循两个排序规则:

1、新建对象过程中,构造体中对final域的初始化写入和这个对象赋值给其他引用变量,这两个操作不能重排序;
2、初次读包含final域的对象引用和读取这个final域,这两个操作不能重排序;(意思就是先赋值引用,再调用final值)
总之上面规则的意思可以这样理解:必需保证一个对象的所有final域被写入完毕后才能引用和读取。这也是内存屏障的起的作用:
1、写final域:在编译器写final域完毕,构造体结束之前,会插入一个StoreStore屏障,保证前面的对final写入对其他线程/CPU可见,并阻止重排序。
2、读final域:在上述规则2中,两步操作不能重排序的机理就是在读final域前插入了LoadLoad屏障。
3、X86处理器中,由于CPU不会对写-写操作进行重排序,所以StoreStore屏障会被省略;而X86也不会对逻辑上有先后依赖关系的操作进行重排序,所以LoadLoad也会变省略。

HappenBefore原则

定义:前一个操作的结果对于后续操作是可见的。在 JMM 中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作必须要存在 happens-before 关系。这两个操作可以是同一个线程,也可以是不同的线程。

JMM 中有哪些方法建立 happen-before 规则:
1、as-if-serial 规则(程序顺序执行):单个线程中的代码顺序不管怎么重排序,对于结果来说是不变的。
2、volatile 变量规则,对于 volatile 修饰的变量的写的操作, 一定 happen-before 后续对于 volatile 变量的读操作;
3、监视器锁规则(monitor lock rule):对一个监视器的解锁,happens-before于随后对这个监视器的加锁。
4、传递性规则:如果A happens-before B,且B happens-before C,那么A happens-before C。
5、start 规则:如果线程 A 执行操作 ThreadB.start(),那么线程 A 的 ThreadB.start()操作 happens-before 线程 B 中的任意操作。
6、join 规则:如果线程 A 执行操作 ThreadB.join()并成功返回,那么线程 B 中的任意操作 happens-before 于线程 A 从 ThreadB.join()操作成功返回。

class VolatileExample {
    int a = 0;
    volatile boolean flag = false;

    public void writer() {
        a = 1;           //1
        flag = true;     //2
    }

    public void reader() {
        if (flag) {       //3
            int i = a;    //4
            ...
        }
    }
}

规则下,假设线程A执行writer()方法之后,线程B执行reader()方法,那么线程B执行4的时候一定能看到线程A写入的值吗?注意,a不是volatile变量。
答案是肯定的。因为根据happens-before规则,我们可以得到如下关系:
根据程序顺序规则,1 happens-before 2;3 happens-before 4。
根据volatile规则,2 happens-before 3。
根据传递性规则,1 happens-before 4。
因此,综合运用程序顺序规则、volatile规则及传递性规则,我们可以得到1 happens-before 4,即线程B在执行4的时候一定能看到A写入的值。

我们再看看实际的操作

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内 存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行。
在这里插入图片描述

JMM三大特性

操作:原子性
即一个操作或者多个操作 要么全部执行并且执行的过程不会被任何因素打断,要么就都不执

public class VolatileAtomicSample {

    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++; //不是一个原子操作,第一轮循环结果是没有刷入主存,这一轮循环已经无效
                    //1 load counter 到工作内存
                    //2 add counter 执行自加
                }
            });
            thread.start();
        }
        try {
            Thread.sleep(1000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        System.out.println(counter);
    }
}

启动10个线程,每个线程执行自增步骤,count++ 是非原子性的。volatile保证数据的可见性,同时存在CPU缓存锁机制以及MESI缓存分布式协议,最后打印的值 <= 10000.

操作:可见性
volatile – LOCK缓存行(有且仅有一个线程会占有缓存行) + CPU缓存一致性原则MESI(独占E–>共享S–>修改M—>其他失效I)

public class VolatileVisibilitySample {
    private boolean initFlag = false;
    public void refresh(){
        this.initFlag = true; //普通写操作,(volatile写)
        String threadname = Thread.currentThread().getName();
        System.out.println("线程:"+threadname+":修改共享变量initFlag");
    }

    public void load(){
        String threadname = Thread.currentThread().getName();
        int i = 0;
        while (!initFlag){
            i++;
        }
        System.out.println("线程:"+threadname+"当前线程嗅探到initFlag的状态的改变"+i);
    }
    public static void main(String[] args){
        VolatileVisibilitySample sample = new VolatileVisibilitySample();
        Thread threadA = new Thread(()->{
            sample.refresh();
        },"threadA");
        Thread threadB = new Thread(()->{
            sample.load();
        },"threadB");
        threadB.start();
        try {
             Thread.sleep(2000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
        threadA.start();
    }
}

分析如下: 只会打印 “线程:threadA:修改共享变量initFlag”.
在这里插入图片描述
在这里插入图片描述
因为在线程A里面增加了锁机制,同时CPU自身也存在时间片切片,导致线程上下文切换,initFlag会从内存中读取线程B更新的值。

会把线程B嗅探机制打印出来。打印如下:

线程:threadA:修改共享变量initFlag

线程:threadB当前线程嗅探到initFlag的状态的改变25747425

修改2:使用volatile关键字 ====> JMM缓存一致性原则(MESI) + LOCK缓存行
在这里插入图片描述
操作:有序性
指令重排 —> 内存屏障(volatile禁止重排优化 )

as-if-serial语义的意思是:不管怎么重排序(编译器和处理器为了提高并行度),(单线程)。 即在单线程情况下,不能改变程序运行的结果
程序的执行结果不能被改变。编译器、runtime和处理器都必须遵守as-if-serial语义。

double p = 3.14; //1
double r = 1.0; //2
double area = p * r * r; //3计算面积
上面例子中1,2存在指令重排操作,但是1,2不能和第三步存在指令重排操作,否则将改变程序运行的结果。
指令重排code示例:

/**
 * 并发场景下存在指令重排
 */
public class VolatileReOrderSample {
    private static int x = 0, y = 0;
    private static volatile int a = 0, b =0;
    static Object object = new Object();

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

}

如果不要volatile去增加内存屏障?如何解决?
– 手动增加屏障,通过Unsafe来解决.
loadFence() storeFence fulFence() .
Unsafe通过BootStwp被加载,否则抛异常。JVM的双亲委派机制
通过反射来获取。

public class UnsafeInstance {

    public static Unsafe reflectGetUnsafe() {
        try {
            Field field = Unsafe.class.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            return (Unsafe) field.get(null);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }
}

在这里插入图片描述

总线风暴

问题:大量使用cas和volatile,会有什么问题? 高并发情况下为什么会产生总线风暴?

  1. CAS —> CPU工作内存与主内存产生大量的交互
  2. volatile —> 产生大量的无效的工作内存变量
    在这里插入图片描述
    解决办法:通过加锁减少关键字的使用
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值