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

时间:2008-12-12 13:35:14

标签: java concurrency

多年来,我一直试图理解处理内存模型和并发性的Java规范的part。我不得不承认我惨遭失败。是的'我理解锁和“synchronized”以及wait()和notify()。我可以使用它们,谢谢。我甚至对“易变”的含义有一个模糊的想法。但所有这些都不是来自语言规范 - 而是来自一般经验。

以下是我要问的两个示例问题。我对特定答案并不是那么感兴趣,因为我需要理解答案是如何从规范中得出的(或者可能是我如何得出规范没有答案的结论)。

  • “易变”究竟做了什么?
  • 写入变量原子?它取决于变量的类型吗?

10 个答案:

答案 0 :(得分:33)

我不打算在这里真正回答你的问题 - 相反,我会将你重定向到我看到推荐的关于这个主题的建议的书:Java Concurrency in Practice

一句警告:如果此处 答案,那么预计会有相当多的错误。我不打算发布细节的原因之一是因为我非常确定至少在某些方面弄错了。我的意思是,当我说每个认为自己能够回答这个问题的人实际上有足够严谨来做到正确的时候几乎没有任何不尊重社区的可能性。 (Joe Duffy最近发现了一些令人惊讶的.NET内存模型。如果他能弄错,那么像我们这样的凡人。)


我会在一个方面提供一些见解,因为它经常被误解:

波动性和原子性之间存在差异。人们通常认为原子写是易失性的(即如果写是原子的,你不需要担心内存模型)。那不是真的。

波动性是指一个线程执行读取(逻辑上,在源代码中)是否“看到”另一个线程所做的更改。

Atomicity是关于是否有可能看到 ,只会看到部分变更。

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

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

y可能为0或257.由于原子性约束,它不会是任何其他值(例如256或1)。但是,即使您知道在“挂起时间”中线程2中的代码在线程1中的代码之后执行,也可能存在奇数缓存,内存访问“移动”等。使变量x volatile将修复此问题

我将把剩下的工作留给真正的诚实专家。

答案 1 :(得分:13)

  • volatile变量可以在线程本地缓存,因此不同的线程可以同时看到不同的值; volatile会阻止此(source
  • 写入32位或更小的变量保证是原子的(implied here); longdouble不是这样,尽管64位JVM可能将它们实现为原子操作

答案 2 :(得分:7)

我不会尝试在这里解释这些问题,而是向您推荐Brian Goetz关于此主题的优秀书籍。

这本书是“Java Concurrency in Practice”,可以在Amazon或任何其他排序良好的计算机文献商店找到。

答案 3 :(得分:4)

这是一个很好的链接,可以为您提供一些深入的信息:

http://www.cs.umd.edu/~pugh/java/memoryModel/jsr-133-faq.html

答案 4 :(得分:4)

我最近发现an excellent article解释了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

答案 5 :(得分:1)

上面的其他答案绝对正确,因为你的问题不是为了佯装。

但是,我理解你真正希望得到内幕的痛苦 - 为此我会指出你回到世界编译器和Java的低级前辈 - 即汇编,C和C ++。

了解不同类型的障碍('栅栏')。了解内存障碍是什么以及在何处需要,将有助于您直观地了解volatile的作用。

答案 6 :(得分:0)

一个概念可能有用:数据(数据)和副本。

如果你声明一个变量,让我们说一个字节,它位于内存中的某个地方,在数据段中(粗略地说)。内存中有8位用于存储该信息。

但是,可以在您的计算机中移动该数据的多个副本。出于各种技术原因,例如线程的本地存储,编译器优化。如果我们有多个副本,它们可能会不同步。

所以你应该始终记住这个概念。不仅对于java类字段,而且对于cpp变量,数据库记录(记录状态数据被复制到多个会话等)也是如此。变量,隐藏/可见副本以及微妙的同步问题将永远存在。

答案 7 :(得分:0)

另一种尝试,从这里和其他来源的答案中提供我理解的事物的摘要(第一次尝试相当远离基础。我希望这个更好)。

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

简而言之,如果你获得了对互斥锁的锁定,那么之前释放该互斥锁的任何线程所写的任何内容都将对你的线程可见。

如果您读取了一个volatile变量,那么在读取它之前写入该volatile变量的任何内容都会被读取线程看到。此外,在写入变量之前写入变量的线程所做的任何对volatile变量的写操作都是可见的。此外,在Java 1.5中,任何写入,无论是volatile还是not,都发生在写入volatile变量之前写入volatile变量的任何线程上都会显示。

构造对象后,可以将其传递给另一个线程,并且所有最终成员都将在新线程中可见并完全构造。关于非最终成员没有类似的保证。这让我觉得对最终成员的赋值充当了对volatile变量(内存栅栏)的写入。

线程在Runnable退出之前编写的任何内容对于执行join()的线程都是可见的。执行start()之前线程编写的任何内容都将对生成的线程可见。

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

这些都不是新的,其他答案都说得更好。我只想写清楚我的头脑。

答案 8 :(得分:0)

这使用城市(线程)和行星(主内存)对其进行了解释。

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

没有从城市到城市的直航。

您必须先前往另一个星球(在本例中为火星),然后再前往您本星球上的另一个城市。因此,从纽约到东京,您必须去:

纽约->火星->东京

现在用2个线程替换NYC和Tokyo,用主内存替换Mars,将飞行作为获取/释放锁,然后您有了JMM。

答案 9 :(得分:0)

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 { }

*注意:

  • thread stack 包含仅局部变量
  • 成员(类和实例变量)存储在 heap 上,即使它们是基元
<块引用>

“volatile”究竟是做什么的?

[Java volatile]

<块引用>

写入变量是原子的吗?它取决于变量的类型吗?

[Atomic variable]