极客时间已完结课程限时免费阅读

38 | 高速缓存(下):你确定你的数据更新了么?

38 | 高速缓存(下):你确定你的数据更新了么?-极客时间

38 | 高速缓存(下):你确定你的数据更新了么?

讲述:徐文浩

时长12:01大小10.98M

在我工作的十几年里,写了很多 Java 的程序。同时,我也面试过大量的 Java 工程师。对于一些表示自己深入了解和擅长多线程的同学,我经常会问这样一个面试题:“volatile 这个关键字有什么作用?”如果你或者你的朋友写过 Java 程序,不妨来一起试着回答一下这个问题。
就我面试过的工程师而言,即使是工作了多年的 Java 工程师,也很少有人能准确说出 volatile 这个关键字的含义。这里面最常见的理解错误有两个,一个是把 volatile 当成一种锁机制,认为给变量加上了 volatile,就好像是给函数加了 sychronized 关键字一样,不同的线程对于特定变量的访问会去加锁;另一个是把 volatile 当成一种原子化的操作机制,认为加了 volatile 之后,对于一个变量的自增的操作就会变成原子性的了。
// 一种错误的理解,是把volatile关键词,当成是一个锁,可以把long/double这样的数的操作自动加锁
private volatile long synchronizedValue = 0;
// 另一种错误的理解,是把volatile关键词,当成可以让整数自增的操作也变成原子性的
private volatile int atomicInt = 0;
amoticInt++;
事实上,这两种理解都是完全错误的。很多工程师容易把 volatile 关键字,当成和锁或者数据数据原子性相关的知识点。而实际上,volatile 关键字的最核心知识点,要关系到 Java 内存模型(JMM,Java Memory Model)上。
虽然 JMM 只是 Java 虚拟机这个进程级虚拟机里的一个内存模型,但是这个内存模型,和计算机组成里的 CPU、高速缓存和主内存组合在一起的硬件体系非常相似。理解了 JMM,可以让你很容易理解计算机组成里 CPU、高速缓存和主内存之间的关系。

“隐身”的变量

我们先来一起看一段 Java 程序。这是一段经典的 volatile 代码,来自知名的 Java 开发者网站dzone.com,后续我们会修改这段代码来进行各种小实验。
public class VolatileTest {
private static volatile int COUNTER = 0;
public static void main(String[] args) {
new ChangeListener().start();
new ChangeMaker().start();
}
static class ChangeListener extends Thread {
@Override
public void run() {
int threadValue = COUNTER;
while ( threadValue < 5){
if( threadValue!= COUNTER){
System.out.println("Got Change for COUNTER : " + COUNTER + "");
threadValue= COUNTER;
}
}
}
}
static class ChangeMaker extends Thread{
@Override
public void run() {
int threadValue = COUNTER;
while (COUNTER <5){
System.out.println("Incrementing COUNTER to : " + (threadValue+1) + "");
COUNTER = ++threadValue;
try {
Thread.sleep(500);
} catch (InterruptedException e) { e.printStackTrace(); }
}
}
}
}
我们先来看看这个程序做了什么。在这个程序里,我们先定义了一个 volatile 的 int 类型的变量,COUNTER。
然后,我们分别启动了两个单独的线程,一个线程我们叫 ChangeListener。另一个线程,我们叫 ChangeMaker。
ChangeListener 这个线程运行的任务很简单。它先取到 COUNTER 当前的值,然后一直监听着这个 COUNTER 的值。一旦 COUNTER 的值发生了变化,就把新的值通过 println 打印出来。直到 COUNTER 的值达到 5 为止。这个监听的过程,通过一个永不停歇的 while 循环的忙等待来实现。
ChangeMaker 这个线程运行的任务同样很简单。它同样是取到 COUNTER 的值,在 COUNTER 小于 5 的时候,每隔 500 毫秒,就让 COUNTER 自增 1。在自增之前,通过 println 方法把自增后的值打印出来。
最后,在 main 函数里,我们分别启动这两个线程,来看一看这个程序的执行情况。程序的输出结果并不让人意外。ChangeMaker 函数会一次一次将 COUNTER 从 0 增加到 5。因为这个自增是每 500 毫秒一次,而 ChangeListener 去监听 COUNTER 是忙等待的,所以每一次自增都会被 ChangeListener 监听到,然后对应的结果就会被打印出来。
Incrementing COUNTER to : 1
Got Change for COUNTER : 1
Incrementing COUNTER to : 2
Got Change for COUNTER : 2
Incrementing COUNTER to : 3
Got Change for COUNTER : 3
Incrementing COUNTER to : 4
Got Change for COUNTER : 4
Incrementing COUNTER to : 5
Got Change for COUNTER : 5
这个时候,我们就可以来做一个很有意思的实验。如果我们把上面的程序小小地修改一行代码,把我们定义 COUNTER 这个变量的时候,设置的 volatile 关键字给去掉,会发生什么事情呢?你可以自己先试一试,看结果是否会让你大吃一惊。
private static int COUNTER = 0;
没错,你会发现,我们的 ChangeMaker 还是能正常工作的,每隔 500ms 仍然能够对 COUNTER 自增 1。但是,奇怪的事情在 ChangeListener 上发生了,我们的 ChangeListener 不再工作了。在 ChangeListener 眼里,它似乎一直觉得 COUNTER 的值还是一开始的 0。似乎 COUNTER 的变化,对于我们的 ChangeListener 彻底“隐身”了。
Incrementing COUNTER to : 1
Incrementing COUNTER to : 2
Incrementing COUNTER to : 3
Incrementing COUNTER to : 4
Incrementing COUNTER to : 5
这个有意思的小程序还没有结束,我们可以再对程序做一些小小的修改。我们不再让 ChangeListener 进行完全的忙等待,而是在 while 循环里面,小小地等待上 5 毫秒,看看会发生什么情况。
static class ChangeListener extends Thread {
@Override
public void run() {
int threadValue = COUNTER;
while ( threadValue < 5){
if( threadValue!= COUNTER){
System.out.println("Sleep 5ms, Got Change for COUNTER : " + COUNTER + "");
threadValue= COUNTER;
}
try {
Thread.sleep(5);
} catch (InterruptedException e) { e.printStackTrace(); }
}
}
}
好了,不知道你有没有自己动手试一试呢?又一个令人惊奇的现象要发生了。虽然我们的 COUNTER 变量,仍然没有设置 volatile 这个关键字,但是我们的 ChangeListener 似乎“睡醒了”。在通过 Thread.sleep(5) 在每个循环里“睡上“5 毫秒之后,ChangeListener 又能够正常取到 COUNTER 的值了。
Incrementing COUNTER to : 1
Sleep 5ms, Got Change for COUNTER : 1
Incrementing COUNTER to : 2
Sleep 5ms, Got Change for COUNTER : 2
Incrementing COUNTER to : 3
Sleep 5ms, Got Change for COUNTER : 3
Incrementing COUNTER to : 4
Sleep 5ms, Got Change for COUNTER : 4
Incrementing COUNTER to : 5
Sleep 5ms, Got Change for COUNTER : 5
这些有意思的现象,其实来自于我们的 Java 内存模型以及关键字 volatile 的含义。那 volatile 关键字究竟代表什么含义呢?它会确保我们对于这个变量的读取和写入,都一定会同步到主内存里,而不是从 Cache 里面读取。该怎么理解这个解释呢?我们通过刚才的例子来进行分析。
刚刚第一个使用了 volatile 关键字的例子里,因为所有数据的读和写都来自主内存。那么自然地,我们的 ChangeMaker 和 ChangeListener 之间,看到的 COUNTER 值就是一样的。
到了第二段进行小小修改的时候,我们去掉了 volatile 关键字。这个时候,ChangeListener 又是一个忙等待的循环,它尝试不停地获取 COUNTER 的值,这样就会从当前线程的“Cache”里面获取。于是,这个线程就没有时间从主内存里面同步更新后的 COUNTER 值。这样,它就一直卡死在 COUNTER=0 的死循环上了。
而到了我们再次修改的第三段代码里面,虽然还是没有使用 volatile 关键字,但是短短 5ms 的 Thead.Sleep 给了这个线程喘息之机。既然这个线程没有这么忙了,它也就有机会把最新的数据从主内存同步到自己的高速缓存里面了。于是,ChangeListener 在下一次查看 COUNTER 值的时候,就能看到 ChangeMaker 造成的变化了。
虽然 Java 内存模型是一个隔离了硬件实现的虚拟机内的抽象模型,但是它给了我们一个很好的“缓存同步”问题的示例。也就是说,如果我们的数据,在不同的线程或者 CPU 核里面去更新,因为不同的线程或 CPU 核有着自己各自的缓存,很有可能在 A 线程的更新,到 B 线程里面是看不见的。

CPU 高速缓存的写入

事实上,我们可以把 Java 内存模型和计算机组成里的 CPU 结构对照起来看。
我们现在用的 Intel CPU,通常都是多核的的。每一个 CPU 核里面,都有独立属于自己的 L1、L2 的 Cache,然后再有多个 CPU 核共用的 L3 的 Cache、主内存。
因为 CPU Cache 的访问速度要比主内存快很多,而在 CPU Cache 里面,L1/L2 的 Cache 也要比 L3 的 Cache 快。所以,上一讲我们可以看到,CPU 始终都是尽可能地从 CPU Cache 中去获取数据,而不是每一次都要从主内存里面去读取数据。
这个层级结构,就好像我们在 Java 内存模型里面,每一个线程都有属于自己的线程栈。线程在读取 COUNTER 的数据的时候,其实是从本地的线程栈的 Cache 副本里面读取数据,而不是从主内存里面读取数据。如果我们对于数据仅仅只是读,问题还不大。我们在上一讲里,已经看到 Cache Line 的组成,以及如何从内存里面把对应的数据加载到 Cache 里。
但是,对于数据,我们不光要读,还要去写入修改。这个时候,有两个问题来了。
第一个问题是,写入 Cache 的性能也比写入主内存要快,那我们写入的数据,到底应该写到 Cache 里还是主内存呢?如果我们直接写入到主内存里,Cache 里的数据是否会失效呢?为了解决这些疑问,下面我要给你介绍两种写入策略。

写直达(Write-Through)

最简单的一种写入策略,叫作写直达(Write-Through)。在这个策略里,每一次数据都要写入到主内存里面。在写直达的策略里面,写入前,我们会先去判断数据是否已经在 Cache 里面了。如果数据已经在 Cache 里面了,我们先把数据写入更新到 Cache 里面,再写入到主内存里面;如果数据不在 Cache 里,我们就只更新主内存。
写直达的这个策略很直观,但是问题也很明显,那就是这个策略很慢。无论数据是不是在 Cache 里面,我们都需要把数据写到主内存里面。这个方式就有点儿像我们上面用 volatile 关键字,始终都要把数据同步到主内存里面。

写回(Write-Back)

这个时候,我们就想了,既然我们去读数据也是默认从 Cache 里面加载,能否不用把所有的写入都同步到主内存里呢?只写入 CPU Cache 里面是不是可以?
当然是可以的。在 CPU Cache 的写入策略里,还有一种策略就叫作写回(Write-Back)。这个策略里,我们不再是每次都把数据写入到主内存,而是只写到 CPU Cache 里。只有当 CPU Cache 里面的数据要被“替换”的时候,我们才把数据写入到主内存里面去。
写回策略的过程是这样的:如果发现我们要写入的数据,就在 CPU Cache 里面,那么我们就只是更新 CPU Cache 里面的数据。同时,我们会标记 CPU Cache 里的这个 Block 是脏(Dirty)的。所谓脏的,就是指这个时候,我们的 CPU Cache 里面的这个 Block 的数据,和主内存是不一致的。
如果我们发现,我们要写入的数据所对应的 Cache Block 里,放的是别的内存地址的数据,那么我们就要看一看,那个 Cache Block 里面的数据有没有被标记成脏的。如果是脏的话,我们要先把这个 Cache Block 里面的数据,写入到主内存里面。然后,再把当前要写入的数据,写入到 Cache 里,同时把 Cache Block 标记成脏的。如果 Block 里面的数据没有被标记成脏的,那么我们直接把数据写入到 Cache 里面,然后再把 Cache Block 标记成脏的就好了。
在用了写回这个策略之后,我们在加载内存数据到 Cache 里面的时候,也要多出一步同步脏 Cache 的动作。如果加载内存里面的数据到 Cache 的时候,发现 Cache Block 里面有脏标记,我们也要先把 Cache Block 里的数据写回到主内存,才能加载数据覆盖掉 Cache。
可以看到,在写回这个策略里,如果我们大量的操作,都能够命中缓存。那么大部分时间里,我们都不需要读写主内存,自然性能会比写直达的效果好很多。
然而,无论是写回还是写直达,其实都还没有解决我们在上面 volatile 程序示例中遇到的问题,也就是多个线程,或者是多个 CPU 核的缓存一致性的问题。这也就是我们在写入修改缓存后,需要解决的第二个问题。
要解决这个问题,我们需要引入一个新的方法,叫作 MESI 协议。这是一个维护缓存一致性协议。这个协议不仅可以用在 CPU Cache 之间,也可以广泛用于各种需要使用缓存,同时缓存之间需要同步的场景下。今天的内容差不多了,我们放在下一讲,仔细讲解缓存一致性问题。

总结延伸

最后,我们一起来回顾一下这一讲的知识点。通过一个使用 Java 程序中使用 volatile 关键字程序,我们可以看到,在有缓存的情况下会遇到一致性问题。volatile 这个关键字可以保障我们对于数据的读写都会到达主内存。
进一步地,我们可以看到,Java 内存模型和 CPU、CPU Cache 以及主内存的组织结构非常相似。在 CPU Cache 里,对于数据的写入,我们也有写直达和写回这两种解决方案。写直达把所有的数据都直接写入到主内存里面,简单直观,但是性能就会受限于内存的访问速度。而写回则通常只更新缓存,只有在需要把缓存里面的脏数据交换出去的时候,才把数据同步到主内存里。在缓存经常会命中的情况下,性能更好。
但是,除了采用读写都直接访问主内存的办法之外,如何解决缓存一致性的问题,我们还是没有解答。这个问题的解决方案,我们放到下一讲来详细解说。

推荐阅读

如果你是一个 Java 程序员,我推荐你去读一读 Fixing Java Memory Model 这篇文章。读完这些内容,相信你会对 Java 里的内存模型和多线程原理有更深入的了解,并且也能更好地和我们计算机底层的硬件架构联系起来。
对于计算机组成的 CPU 高速缓存的写操作处理,你也可以读一读《计算机组成与设计:硬件 / 软件接口》的 5.3.3 小节。

课后思考

最后,给你留一道思考题。既然 volatile 关键字,会让所有的数据写入都要到主内存。你可以试着写一个小的程序,看看使用 volatile 关键字和不使用 volatile 关键字,在数据写入的性能上会不会有差异,以及这个差异到底会有多大。
欢迎把你写的程序分享到留言区。如果有困难,你也可以把这个问题分享给你朋友,拉上他一起讨论完成,并在留言区写下你们讨论后的结果。
分享给需要的人,Ta购买本课程,你将得20
生成海报并分享

赞 30

提建议

上一篇
37 | 高速缓存(上):“4毫秒”究竟值多少钱?
下一篇
39 | MESI协议:如何让多核CPU的高速缓存保持一致?
unpreview
 写留言

精选留言(49)

  • LDxy
    2019-07-22
    volatile关键字在用C语言编写嵌入式软件里面用得很多,不使用volatile关键字的代码比使用volatile关键字的代码效率要高一些,但就无法保证数据的一致性。volatile的本意是告诉编译器,此变量的值是易变的,每次读写该变量的值时务必从该变量的内存地址中读取或写入,不能为了效率使用对一个“临时”变量的读写来代替对该变量的直接读写。编译器看到了volatile关键字,就一定会生成内存访问指令,每次读写该变量就一定会执行内存访问指令直接读写该变量。若是没有volatile关键字,编译器为了效率,只会在循环开始前使用读内存指令将该变量读到寄存器中,之后在循环内都是用寄存器访问指令来操作这个“临时”变量,在循环结束后再使用内存写指令将这个寄存器中的“临时”变量写回内存。在这个过程中,如果内存中的这个变量被别的因素(其他线程、中断函数、信号处理函数、DMA控制器、其他硬件设备)所改变了,就产生数据不一致的问题。另外,寄存器访问指令的速度要比内存访问指令的速度快,这里说的内存也包括缓存,也就是说内存访问指令实际上也有可能访问的是缓存里的数据,但即便如此,还是不如访问寄存器快的。缓存对于编译器也是透明的,编译器使用内存读写指令时只会认为是在读写内存,内存和缓存间的数据同步由CPU保证。
    展开
    共 6 条评论
    136
  • Knight²º¹⁸
    2019-12-20
    Java sleep 解释有问题,sleep 并不是说线程有时间去主内存中读取变量,而是 sleep 的线程会让出cpu,线程被唤醒后才会去重新加载变量。

    作者回复: Knight²º¹⁸同学, 你好,你说的有道理,我去修改一下。

    共 7 条评论
    79
  • 林三杠
    2019-07-22
    反复看了几次写回策略,才看明白。主要是“如果我们发现,我们要写入的数据所对应的 Cache Block 里,放的是别的内存地址的数据”这句。同一个cache地址可能被多个进程使用,使用前需要确认是否是自己的数据,是的话,直接写,不是自己的而且被标记为脏数据,需要同步回主内存。老师,我理解的对吧?
    共 8 条评论
    19
  • 王加武
    2019-12-23
    不加volatitle关键字 private static int num = 1; public static void main(String[] args) { int[] arr = new int[8000000]; for (int i = 0; i < 8000000; i++) { arr[i] = num; num++; } 运行时间为:28毫秒 加了关键字 private static volatile int num = 1; public static void main(String[] args) { int[] arr = new int[8000000]; for (int i = 0; i < 8000000; i++) { arr[i] = num; num++; } 运行的时间为128毫秒
    展开

    作者回复: Hash同学, 👍,加油,从实践中有所体会是最好的学习方法。

    共 2 条评论
    17
  • 西门吹牛
    2020-07-08
    volatile 关键字去掉,变量的更新是先从内存中把变量加载到自己的缓存, ChangeMaker 线程把变量COUNTER = 0 加载到自己的缓存,并在自己的缓存内更新。ChangeMaker 每次更新完成后,会进行sleep,此时回把更新的数据同步到内存中。而 ChangeListener 线程,也是先从内存中获取数据,因为他自己的缓存内没有该变量。ChangeListener 线程,第一次从内存中读取到的变量值是0,因为车此时 ChangeMaker 线程对变量的更新还没有同步到内存,ChangeListener 线程从内存读取到0,并把该值加载到缓存,之后进行循环,每次循环都是从自己的缓存中读取数据,所以ChangeListener线程从每次循环从缓存中获取的变量值是0; ChangeListener 线程修改为在循环内Thread.sleep(5)。线程休眠结束后,每次休眠结束,线程都会在从内存中在读取一次数据,这时休眠时间为5秒,刚好ChangeMaker 线程每次更新也休眠5秒,这时,ChangeMaker线程对变量的更新,在同步到内存后,刚好被ChangeListener 线程读取到; 所以,能得出结论,sleep之后,线程有足够的时间将缓存同步到内存,如果没有sleep,线程一直在执行,就没有时间将缓存数据同步到内存,同时,每次sleep之后,线程都会从内存中在读取一次数据到缓存,而不是sleep之后,还是读取自己的缓存数据。 Java 内存模型是一个隔离了硬件实现的虚拟机内的抽象模型,不同的线程或 CPU 核有着自己各自的缓存,缓存会导致可见性问题,可见性是并发bug的源头之一。所以java引入volatile关键字,能解决缓存带来的线程之间可见性的问题。java内存模型中规定,一个线程对volatile修饰变量的写操作先发与另一线程对于该变量的读操作,也就是说,针对volatile修饰的变量,一个线程要想读取到别的线程更新后的数据,就必须从内存中读取,而一个线程的写操作要想被别的线程看到,就必须保证在更新完之后,同步到内存中。所以volatile关键字的作用,就是确保变量的读取和写入,一定会同步到主内存,而不基于cpu缓存中的数据进行读取和写入。 要实现对volatile修饰的变量,每次的读取和写入,一定会同步到主内存。java的实现方案是利用内存屏障来实现,而内存屏障的实现,是基于cpu指令来实现的。经过volatile修饰的变量。在经过jvm解释器解释成机器码后,都会插入一写内存屏障的cpu指令,这些cpu指令的作用就是确保,每次对volatile修饰的变量的更新,都必须同步到内存,而每次读取volatile修饰的变量,都必须从内存中获取,而不是直接从cpu缓存获取。 所以Java内存模型隔离了具体的硬件实现,这些内存屏障的指令都是jvm在解释执行的时候加上的,程序员只需要在代码中用volatile 修饰即可,至于volatile 底层的实现,都是基于 java 的内存模式实现的。
    展开
    共 1 条评论
    9
  • Monday
    2020-06-13
    简而言之,volatile变量就是禁用Cache
    7
  • 曙光
    2019-10-22
    看了后面MESI协议的介绍,反而对本章示例程序有疑问,程序(2)中,虽然去掉了volatile的关键字,但ChangeListener应该接收到“写失效”的广播,然后中断忙等,再去内存获取最新数据。那有没有广播到ChangeListener的cpu cache呢? 本人i5-8250U, ChangeListener需要至少大于等于Thread.sleep(495)才能和程序(1)的测试结果一样,这是咋回事?
    5
  • 树军
    2020-08-06
    老师,这讲里对volatile的解释是完全错的,cache从CPU的角度来看,对程序员是透明的,从软件看过去不会存在不一致的情况,只有在多master访问的时候才会关心,比如DMA等。这里的不一致不是由cache造成的,而是编译器对变量优化造成的,忙等待中,如果没有volatile关键字,编译器认为这个变量不会被改变,分配一个临时变量,一般就是一个寄存器,每次访问都直接访问寄存器,而不去访问真实的地址造成的
    共 3 条评论
    5
  • Q罗
    2020-06-10
    讲解思路清晰,并且很实用,“所谓脏的,就是指这个时候,我们的 CPU Cache 里面的这个 Block 的数据,和主内存是不一致的。”这个解释很到位。
    3
  • 阿锋
    2019-07-22
    上面的流程图中,有一步是从主内存读取数据到cache block 我觉得这一步是多余的,因为下面接下来的一步是写入数据到cache block,之后都要写入新数据了,为啥还要读,不理解?
    共 5 条评论
    3
  • 一頭蠻牛
    2022-07-05
    老师讲的很好 唯一就是不该用Java来距离 增加了理解的复杂程度
    2
  • 静静聆听
    2020-10-31
    老师,不带valotile的关键字未更新,是java编译器,jit做的优化,jit会认为数据没有变更,优化成不再去跟cpu交互读取数据了,就是说没带valotile的变量,数据只读取了一次
    2
  • 许先森
    2020-01-17
    文中有写:“写回策略的过程是这样的:如果发现我们要写入的数据,就在 CPU Cache 里面,那么我们就只是更新 CPU Cache 里面的数据。同时,我们会标记 CPU Cache 里的这个 Block 是脏(Dirty)的。所谓脏的,就是指这个时候,我们的 CPU Cache 里面的这个 Block 的数据,和主内存是不一致的。” 所以图里的Cache Block指的是CPU高速缓存块,不是内存块。

    作者回复: 许先森同学, 是的,这里指的是Cache,不是主内存。

    2
  • 花晨少年
    2019-12-15
    讲得好啊,透彻

    作者回复: 🙏 谢谢支持

    2
  • 随心而至
    2019-10-22
    比如,下面的程序,volatile变量count 每次都要写回到内存(Memory)中,而sum是线程栈的本地变量, 每次都只会写到线程栈中(可以对应CPU Cache)。所以最后add()/add2()方法的耗时,近似于Memory访问速度/CPU Cache访问速度。 public class VolatilePerformanceTest { private static volatile int count =0; /** * count++注意并不是原子操作。 */ public static void add(){ for(long i=0; i< 100000000; i++){ count++; } } public static void add2(){ int sum = 0; for(int i=0; i< 100000000; i++){ sum++; } } public static void main(String[] args){ long start =System.currentTimeMillis(); //753 ms add(); System.out.println(System.currentTimeMillis() - start); start= System.currentTimeMillis(); //7ms add2(); System.out.println(System.currentTimeMillis() - start); } }
    展开
    2
  • WL
    2019-07-24
    请问老师在写回策略中检查Cache Block是不是别的内存地址的数据是啥意思, 一个cache block不是对应一个内存block吗?
    共 4 条评论
    2
  • humor
    2019-07-22
    写回的内存写入策略的那张图中,为什么会有从主内存读取数据到cache block这一步呢?反正读入了主内存的数据也要被当前的数据覆盖掉的
    共 2 条评论
    2
  • ano
    2021-02-02
    volatile为什么不 从cpu的 L3 cache里读写?L3 cache是多个core共享的,同样可以解决这个并发的问题
    1
  • 仰望星空
    2020-08-08
    对未使用volatile修饰的共享变量,ChangeMaker在其Cache中对该变量进行修改,要想在ChangeListener能监听到,如下两个条件缺一不可: 1、ChangeMaker中对变量修改后,必须调用sleep,这样当前CPU核才有空闲时间将修改后的变量值同步到主内存。 2、ChangeListener在读变量的过程中,也必须sleep,这样当前CPU核才有空闲时间从主内存中重新加载变量值。 说到底,这也是JMM的一种策略
    展开
    1
  • Dovelol
    2020-07-06
    老师好,想问一下,java内存模型里面的每个线程的缓存是不是就是cpu的cache呢?还是说java也实现了一套类似于cpu,l1cache的模型?
    共 1 条评论
    1