Java内存模型 - 有人能解释一下吗?

33
多年以来,我一直试图理解Java规范中处理内存模型和并发的部分。但我不得不承认,我已经惨败了。是的,我了解锁定、"synchronized"、wait()和notify()。并且我可以很好地使用它们,谢谢。我甚至对"volatile"做了一个模糊的了解。但所有这些都不是从语言规范中得出的——而是从一般经验中得出的。
以下是两个我提出的样例问题。我关心的并不是特定的答案,而是需要了解答案是如何从规范中得出的(或者我如何得出规范没有答案的结论)。
  • "volatile"到底是做什么用的?
  • 变量的写入是否是原子性的?这是否取决于变量的类型?
10个回答

35

我不打算在这里试图回答你的问题 - 相反,我会将你重定向到一本书,这本书被认为是关于这个主题建议的:《Java并发实践》

有一个警告:如果这里确实有答案,那么预计其中很多都是错误的。我不发表详细信息的原因之一是因为我非常确定我至少在某些方面上会出错。当我说每个人都认为他们可以回答这个问题实际上具有足够严谨性获得正确答案的机会几乎为零时,我绝不想不尊重社区。(Joe Duffy最近发现了.NET内存模型的一部分,他感到很惊讶。如果他会犯错,我们这样的凡人也会。)


我将提供一些洞见,因为它经常被误解:

易变性和原子性有所区别。人们经常认为原子写入是易变的(即,如果写入是原子的,就不需要担心内存模型)。这是不正确的。

易变性是指一个线程执行读取操作(在源代码中逻辑上)是否会“看到”其他线程所做的更改。

原子性是指是否存在任何可能,如果发生更改,则只会看到更改的一部分。

例如,对整数字段进行写入是保证原子性的,但不是易变的。这意味着如果我们有以下代码(从foo.x = 0开始):

Thread 1: foo.x = 257;
Thread 2: int y = foo.x;

对于变量y,它可以是0或257。由于原子性约束,它不可能是其他任何值(例如256或1)。但是,即使您知道线程2中的代码在“墙上时间”中在线程1中的代码之后执行,仍可能存在奇怪的缓存、内存访问“移动”等情况。将变量x定义为易失性将解决这个问题。

其余部分交给真正的专家们。


14
  • volatile变量可以被线程本地缓存,因此不同的线程可能同时看到不同的值;使用volatile可以防止这种情况发生(来源)
  • 对于32位或更小的变量的写入是保证原子性的(在此暗示);但longdouble不是,尽管64位JVM可能将它们实现为原子操作

7
我不会在这里尝试解释这些问题,而是向您推荐Brian Goetz关于这个主题的优秀著作。
这本书叫做“Java并发实践”,可以在亚马逊或任何其他计算机文献精选商店找到。

4

谢谢提供链接!这并不能替代一本书(我会去买的),但它给了我一个见解:我通常想到同步,而内存模型更关注重新排序。我需要学会区分两者,并思考后者。 - user3458

4
我最近发现了一篇优秀的文章,它解释了volatile的含义:
首先,您需要了解Java内存模型的一些内容。多年来,我一直在努力简要而准确地解释它。截至今天,我能想到的最好方法是这样描述它:
- Java中的每个线程都在单独的内存空间中运行(显然不是真的,所以请原谅我)。 - 您需要使用特殊机制来保证这些线程之间进行通信,就像在消息传递系统中一样。 - 在一个线程中发生的内存写入可能会“泄漏”,并被另一个线程看到,但这绝不是保证的。如果没有明确的通信,您无法保证哪些写入会被其他线程看到,甚至无法保证它们被看到的顺序。
Java volatile修饰符是一种特殊机制的示例,用于保证线程之间进行通信。当一个线程写入一个volatile变量,并且另一个线程看到该写入时,第一个线程告诉第二个线程有关执行写入该volatile变量之前的所有内存内容的信息。
附加链接: http://jeremymanson.blogspot.com/2008/11/what-volatile-means-in-java.html http://www.javaperformancetuning.com/news/qotm030.shtml

1
不是N,而是N+1!请看我的答案 :) - user3458

2

JVM内存模型

高级别图示

代码示例

class MainClass {
    void method1() { //<- main
        int variable1 = 1;
        Class1 variable2 = new Class1();

        variable2.method2();
    }
}

class Class1 {
    static Class2 classVariable4 = new Class2();
    int instanceVariable5 = 0;
    Class2 instanceVariable6 = new Class2();

    void method2() {
        int variable3 = 3;
    }
}

class Class2 { }

*注意:

  • 线程堆栈只包含本地变量
  • 成员变量(类和实例变量)即使是基本类型也存储在

"volatile"到底是什么作用?

[Java volatile]

对变量的写操作是原子的吗?这取决于变量的类型吗?

[原子变量]

[Java本地变量的线程安全性]


1

以上其他答案都是正确的,因为你的问题并不是那么容易解决。

然而,我理解你想要深入了解底层的痛苦 - 对此,我会指向世界上的编译器和 Java 的低级前身 - 即汇编语言、C 和 C++。

阅读有关不同类型障碍(“栅栏”)的文章。了解什么是内存栅栏,以及何时需要它,将帮助您直观地掌握 volatile 的作用。


0

这是我从这里和其他来源理解到的事情的另一次总结尝试(第一次尝试相当离谱,希望这次更好)。

Java内存模型是关于将一个线程写入内存的值传播到其他线程,以便其他线程在从内存读取时可以看到它们。

简而言之,如果您在互斥锁上获得锁定,则在此之前释放该互斥锁的任何线程写入的任何内容都将对您的线程可见。

如果您读取一个易失变量,则在您读取它之前写入该易失变量的任何内容都将对读取线程可见。此外,在写入您的变量之前写入您的变量的线程执行的任何对易失变量的写入也是可见的。此外,在Java 1.5中,任何写入易失或非易失变量的任何线程发生在写入易失变量之前,都将对您可见。

在构造对象后,您可以将其传递给另一个线程,并且所有最终成员都将在新线程中可见并完全构造。对于非最终成员没有类似的保证。这使我认为对最终成员的赋值就像对易失变量(内存屏障)的写入。

在其Runnable退出之前,线程写入的任何内容都可被执行join()的线程看到。 在执行start()之前,线程写入的任何内容将对生成的线程可见。

另一个需要提及的事情是:volatile变量和同步具有一个很少被提到的功能:除了刷新线程缓存并提供一次只访问外,它们还可以防止编译器和CPU在同步边界上重新排序读取和写入。

这些都不是新鲜事物,其他答案已经更好地阐述了它。 我只是想写下来以清理我的思路。


0
一个概念可能有所帮助:数据(datum)和副本。
如果您声明一个变量,比如一个字节,它住在内存的某个地方,即数据段(粗略地说)。有8位在内存中专门用于存储那个信息碎片。
然而,在您的计算机中可以有多个复制品在移动。由于各种技术原因,例如线程本地存储、编译器优化等。如果我们有几个副本,它们可能会不同步。
所以您应该始终牢记这一概念。这不仅适用于Java类字段,还适用于cpp变量、数据库记录(记录状态数据被复制到多个会话等)。变量、它们的隐藏/可见副本和微妙的同步问题将永远存在。

0

这是用城市(线程)和星球(主存)来解释的。

http://mollypages.org/tutorials/javamemorymodel.mp

城市之间没有直达航班。

你必须先去另一个星球(在这个例子中是火星),然后到家乡星球的另一个城市。所以,从纽约到东京,你必须经过以下步骤:

纽约 -> 火星 -> 东京

现在用2个线程替换纽约和东京,用主内存替换火星,将获取/释放锁定作为航班,这就是Java内存模型。


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