线程缓存和Java内存模型

32

我正在尝试理解Java内存模型和线程。据我所知,每个线程都有一个“主”内存的本地副本。因此,如果一个线程尝试更改某个对象的int变量,它会缓存int变量,如果它更改了变量,则其他线程可能看不到更改。

但是如果线程缓存某个对象而不是int呢?在这种情况下,线程会缓存什么?如果一个线程缓存对对象的引用,则任何对对象状态的更改都不可见于其他线程?为什么?

4个回答

36

CPU有不同级别的缓存L1、L2和L3。每个CPU(以及可能的CPU核心)都有自己的缓存。这些缓存为性能存储了最小的主存储器(RAM)集。

  _______________    ______________  
 |     CPU 1     |  |     CPU 2    |  
 |   _________   |  |   _________  |  
 |  | Level 1 |  |  |  | Level 1 | |  
 |  |   Cache |  |  |  |  Cache  | |  
 |  |         |  |  |  |         | |
 |  |_________|  |  |  |_________| |  
 |_______________|  |______________|
           | |              | |
           | |              | |
          _|_|______________|_|__
         |                       |
         |      MAIN MEMORY      | 
         |_______________________|


  Time     Command                 CPU 1 (Cache)      CPU 2 (Cache)        Main Memory     
-------  ----------              ----------------    --------------       -------------
  1          ---                       ---                ---                x = 10
  2       Read x  (on cpu1)           x = 10              ---                x = 10
  3       Write x <--20 (on cpu1)     x = 20              ---                x = 10       
  4       Read  x (on cpu2)           x = 20              x = 10             x = 10
  5       put cache to Main mem       x = 20              x = 10             x = 20
例如,以上执行顺序,在CPU2上x的值是错误的。x的值已经被CPU1改变了。 如果将x变量定义为volatile,所有写操作会立即反映到主内存中。

1
这是不正确的。缓存始终是一致的,因此在CPU提交值到缓存后,不可能出现另一个CPU仍然可以看到旧值的情况。像MESI这样的缓存一致性算法确保您上面的解释永远不会发生。因此,易失性值根本不需要写入主内存。它可能会无限期地留在缓存中。有关缓存实现和内存模型的更多信息,请查看以下书籍(免费):https://www.morganclaypool.com/doi/abs/10.2200/S00346ED1V01Y201104CAC016 - pveentjer

13

CPU有多个缓存。这些硬件缓存可能会有不一致的数据副本。它们可能不一致的原因是,保持一致会使您的代码速度减慢10倍,并破坏您从多个线程获得的任何好处。要获得良好的性能,您需要选择性一致性。Java内存模型描述了什么时候确保数据一致,但在简单情况下并不如此。

注意:这不仅是CPU问题。一个不需要在线程之间保持一致的字段可以内联在代码中。这意味着如果一个线程更改了值,另一个线程可能永远不会看到这个更改,因为它已被固化到代码中。


2
@Andremoniy JLS(Java 语言规范)讨论了虚拟机的寄存器栈。它并没有涉及 CPU 的实际寄存器或缓存,因为这些都是实现细节。 - Peter Lawrey
哇,非常感谢。这是一个非常重要的观点。我敢问您能否看一下这个问题?https://dev59.com/S6_la4cB1Zd3GeqPxsHR - Andremoniy
1
我只知道一个具有不相干缓存的微处理器,那就是GPU。否则,缓存总是一致的。 - pveentjer

12

=============================================================

以下答案有很多错误。请不要将其用于任何其他目的,只是为了好玩。现代CPU上的缓存总是一致的。

=============================================================

一个线程没有内存的本地副本。线程读取/写入的部分内存可能来自缓存,而不是主内存。缓存不需要彼此同步,也不需要与主内存同步。因此,这就是您可以观察到不一致性的地方。

因此,如果一个线程尝试更改某个对象的int变量,它会缓存int变量,如果更改了其他线程可能无法看到更改。

那是对的。 Java内存模型在先前发生规则中定义,例如,volatile对字段x的写入和对字段x的volatile读取之间存在先前发生规则。因此,当写入完成时,后续的读取将看到写入的值。

如果没有这样的先前发生关系,则一切都无法确定(当没有先前发生规则时,指令重排也会使事情变得复杂)。

如果线程缓存了对对象的引用,则对对象状态的任何更改对其他线程也不可见? 为什么?

它可能是可见的...也可能不可见。如果没有先前发生规则,一切都无法确定。原因是否则许多优化(例如硬件技巧来加快速度或编译器技巧)都将不被允许。当然,始终保持内存与缓存同步会降低性能。

===========================================================


2
请注意,“subsequent”并不完全等同于“write后发生的”。volatile无法提供任何时间性保证,它只能保证一致性,即不会出现观测到的写入顺序混乱。 - Marko Topolnik
@pveentjer 为什么你说它可能是可见的,也可能不可见?只有在线程本地堆栈中缓存的引用才能被缓存。因此,更改应该在线程之间可见。我错了吗?硬件/编译器技巧 - 你能否给出更清晰的图片? - kiranpradeep
@Kiran JMM适用于任何变量,对象引用并没有特殊性。组成对象状态的只是一堆变量。 - Holger

2

然而,在您编写良好的多线程代码之前,您真正需要研究更多关于多线程代码的复杂性和微妙之处。

当涉及到线程时,很少有什么是保证的。

您能想象出当两个不同的线程访问单个类实例并且这些线程调用该对象上的方法且这些方法修改对象状态时可能发生的混乱吗?…即使想象都太可怕了。" ——摘自《Java 6认证程序员》第9章:线程。

我的朋友,

在Java中,线程不会缓存任何对象或变量,它们只是对一个对象实例的引用。谈论线程缓存内存更像是谈论操作系统线程... Java在所有操作系统中以相同的方式工作,无论线程在内部如何管理,这因不同的操作系统而异。

看一下这段代码:

AccountDanger r = new AccountDanger();
Thread one = new Thread(r):
Thread two = new Thread(r);

正如您所看到的,在这种情况下,线程可以访问同一个实例:r。那么,您肯定会遇到同步问题......无论我们谈论的是本地或对象成员,线程一和二都将访问r的所有成员(如果它们通过作用域或设置器/获取器可访问)并直接从r实例中读取值。即使您没有注意到它,这也是肯定的,有时确实很难发现。
如果您想编写多线程应用程序,请阅读有关Java作用域Java同步的内容。
此致敬礼

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接