【问题标题】:Java memory model - can someone explain it?Java 内存模型 - 有人可以解释一下吗?
【发布时间】:2008-12-12 13:35:13
【问题描述】:

多年来,我一直试图理解处理内存模型和并发性的 Java 规范的 part。我不得不承认我失败得很惨。是的'我了解锁和“同步”以及等待()和通知()。我可以很好地使用它们,谢谢。我什至对“易失性”的作用有一个模糊的概念。但所有这些都不是来自语言规范,而是来自一般经验。

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

  • “volatile”到底有什么作用?
  • 对变量的写入是原子的吗?是否取决于变量的类型?

【问题讨论】:

    标签: java concurrency


    【解决方案1】:

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

    一句警告:如果这里有个答案,预计其中有不少是错误的。我不打算发布详细信息的原因之一是因为我很确定我会至少在某些方面弄错了。当我说每个认为自己可以回答这个问题的人实际上有足够的严谨性来正确回答这个问题时,我的意思是没有任何不尊重社区的意思。 (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 volatile 可以解决此问题。

    我会把剩下的交给真正的诚实的专家。

    【讨论】:

      【解决方案2】:
      • 非volatile变量可以在线程本地缓存,所以不同的线程可能同时看到不同的值; volatile 防止这种情况发生 (source)
      • 对 32 位或更小的变量的写入保证是原子的 (implied here); long 和 double 并非如此,尽管 64 位 JVM 可能将它们实现为原子操作

      【讨论】:

        【解决方案3】:

        我不会尝试在这里解释这些问题,而是请您参考 Brian Goetz 关于该主题的优秀书籍。

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

        【讨论】:

          【解决方案4】:

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

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

          【讨论】:

          • 感谢您的链接!它不会取代一本书(我会得到),但它确实给了我一个见解:我习惯于考虑同步,而内存模型更关心重新排序。我需要学会区分两者,并考虑最后一个。
          【解决方案5】:

          我最近发现 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

          【讨论】:

          • 不是N,是N+1!看看我自己的答案:)
          【解决方案6】:

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

          但是,我理解您对真正想要了解底层内容的痛苦 - 为此,我会向您指出世界上的编译器和 Java 的低级前辈 - 即汇编、C 和 C++。

          了解不同类型的障碍(“栅栏”)。了解什么是内存屏障以及它在哪里是必要的,将有助于您直观地掌握 volatile 的作用。

          【讨论】:

            【解决方案7】:

            一个概念可能会有所帮助:数据(基准)和副本。

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

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

            因此,您应该始终牢记这一概念。这不仅适用于 java 类字段,而且适用于 cpp 变量、数据库记录(记录状态数据被复制到多个会话等)。变量、它们的隐藏/可见副本以及微妙的同步问题将永远存在。

            【讨论】:

              【解决方案8】:

              另一个尝试提供我从这里的答案和其他来源中理解的内容的摘要(第一次尝试离基地很远。我希望这个更好)。

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

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

              如果您读取 volatile 变量,则在读取之前写入该 volatile 变量的任何内容对读取线程都是可见的。此外,在写入变量之前写入变量的线程对 volatile 变量的任何写入都是可见的。此外,在 Java 1.5 中,在写入 volatile 变量之前发生在写入 volatile 变量的任何线程上的任何写入,无论是否 volatile,您都将是可见的。

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

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

              另一件要提的事情:易失性变量和同步有一个很少被提及的功能:除了刷新线程缓存和提供一次一个线程的访问之外,它们还可以防止编译器和 CPU 重新排序跨同步边界的读取和写入.

              这些都不是新的,其他答案更好地说明了这一点。我只是想写下这个来理清头绪。

              【讨论】:

                【解决方案9】:

                这使用城市(线程)和行星(主内存)来解释它。

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

                从一个城市到另一个城市没有直飞航班。

                你必须先去另一个星球(这里是火星),然后去你家乡星球上的另一个城市。所以,从纽约到东京,你必须去:

                纽约 -> 火星 -> 东京

                现在用 2 个线程替换 NYC 和 Tokyo,用 Main memory 替换 Mars,将作为获取/释放锁的航班替换为 JMM。

                【讨论】:

                  【解决方案10】:

                  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 上,即使它们是基元

                  “易失性”到底是做什么的?

                  [Java volatile]

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

                  [Atomic variable]

                  [Java thread safe of local variables]

                  【讨论】:

                    猜你喜欢
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    • 2011-11-22
                    • 2016-12-12
                    • 2011-03-02
                    • 1970-01-01
                    • 1970-01-01
                    • 1970-01-01
                    相关资源
                    最近更新 更多