【问题标题】:Memory Consistency Errors vs Thread interference内存一致性错误与线程干扰
【发布时间】:2010-09-03 00:42:59
【问题描述】:

内存一致性错误和线程干扰有什么区别? 使用同步来避免它们有何不同?请举例说明。我无法从 sun Java 教程中得到这个。任何阅读材料的建议以纯粹在 java 的上下文中理解这一点都会有所帮助。

【问题讨论】:

    标签: java multithreading concurrency


    【解决方案1】:

    内存一致性错误不能纯粹在java的上下文中理解——多cpu系统上共享内存行为的细节是高度特定于架构的,更糟糕​​的是,x86与从一开始就为多处理器机器设计的架构(如 POWER 和 SPARC)相比,(今天大多数编码人员学习编码的地方)具有非常适合程序员的语义,因此大多数人真的不习惯考虑内存访问语义。

    我将举一个常见的例子,说明内存一致性错误可能会给您带来麻烦。假设对于这个例子,x 的初始值为 3。几乎所有架构都保证如果一个 CPU 执行代码:

    STORE 4 -> x     // x is a memory address
    STORE 5 -> x 
    

    另一个 CPU 执行

    LOAD x
    LOAD x
    

    将看到3,33,44,44,55,5 从它的两个LOAD 指令的角度来看。基本上,CPU 保证从所有 CPU 的角度来看,对单个内存位置的写入顺序保持不变,即使允许其他 CPU 知道每个写入的确切时间有所不同。

    CPU 彼此不同的地方往往在于它们对涉及不同内存地址的LOADSTORE 操作做出的保证。假设对于这个例子,xy 的初始值都是 4。

    STORE 5 -> x   // x is a memory address
    STORE 5 -> y // y is a different memory address
    

    然后另一个 CPU 执行

    LOAD x
    LOAD y
    

    在这个例子中,在某些架构上,第二个线程可以看到4,45,54,5、或5,4。哎哟!

    大多数架构以 32 位或 64 位字的粒度处理内存 - 这意味着在 32 位 POWER/SPARC 机器上,您无法更新 64 位整数内存位置并安全地从另一个没有显式同步的线程ever。傻了吧?

    线程干扰要简单得多。基本思想是java不保证java代码的单个语句原子执行。例如,增加一个值需要读取该值,增加它,然后再次存储它。因此,在两个线程执行 x++ 之后,您可以拥有 int x = 1x 最终可能为 23,具体取决于较低级别代码的交错方式(这里工作的较低级别抽象代码大概看起来比如LOAD x, INCREMENT, STORE x)。这里的基本思想是,java 代码被分解成更小的原子片段,除非你明确使用同步原语,否则你不能假设它们是如何交错的。

    有关更多信息,请查看this 论文。它又长又干,是一个臭名昭著的混蛋写的,但是嘿,它也很不错。还可以查看this(或者只是谷歌“双重检查锁定被破坏”)。这些内存重新排序问题引起了许多 C++/java 程序员的丑陋,他们在几年前试图让他们的单例初始化变得有点过于聪明。

    【讨论】:

    【解决方案2】:

    线程干扰是关于线程覆盖彼此的语句(例如,线程 A 增加一个计数器而线程 B 减少它同时),导致计数器的实际值不可预测的情况.您可以通过强制独占访问来避免它们,一次一个线程。

    另一方面,内存不一致与可见性有关。线程 A 可能会增加 counter,但随后线程 B 可能不知道此更改尚未,因此它可能会读取一些先前的值。你可以通过建立一个happens-before关系来避免它们,即

    只是保证一个特定语句写入的内存对另一个特定语句可见。(每Oracle

    【讨论】:

      【解决方案3】:

      要阅读的文章是 Adve 和 Boehm 在 2010 年 8 月的第一卷中的“内存模型:重新思考并行语言和硬件的案例”。 53 第 8 期 ACM 通讯。计算机机械协会会员 (http://www.acm.org) 可在线获取此信息。这涉及一般问题,还讨论了 Java 内存模型。

      有关 Java 内存模型的更多信息,请参阅http://www.cs.umd.edu/~pugh/java/memoryModel/

      【讨论】:

        【解决方案4】:

        内存一致性问题通常表现为先发制人的关系破裂。

        Time A: Thread 1 sets int i = 1
        Time B: Thread 2 sets i = 2
        Time C: Thread 1 reads i, but still sees a value of 1, because of any number of reasons that it did not get the most recent stored value in memory.
        

        您可以通过在变量上使用volatile 关键字或使用java.util.concurrent.atomic 包中的AtomicX 类来防止这种情况发生。这些消息中的任何一条都确保没有第二个线程会看到部分修改的值,并且没有人会看到不是内存中最新实际值的值。

        (同步 getter 和 setter 也可以解决问题,但对于不知道为什么这样做的其他程序员来说可能看起来很奇怪,并且在面对绑定框架和持久性框架等使用反射。)

        --

        线程交错是指两个线程将一个对象组合起来并看到不一致的状态。

        我们有一个带有 itemQuantity 和 itemPrice 的 PurchaseOrder 对象,自动逻辑生成发票总额。

        Time 0: Thread 1 sets itemQuantity 50
        Time 1: Thread 2 sets itemQuantity 100
        Time 2: Thread 1 sets itemPrice 2.50, invoice total is calculated $250
        Time 3: Thread 2 sets itemPrice 3, invoice total is calculated at $300
        

        线程 1 执行了一个不正确的计算,因为其他线程在他的操作之间弄乱了对象。

        您可以通过使用synchronized 关键字来解决此问题,以确保一次只有一个人可以执行整个过程,或者使用java.util.concurrent.locks 包中的锁。使用 java.util.concurrent 通常是新程序的首选方法。

        【讨论】:

        • 您的第一个示例没有意义,因为不能保证在多个 CPU 上与操作相关的确定性单个时钟。此外,几乎所有常用的 CPU 都保证在单个内存地址方面不违反发生之前的情况 - 您需要多个内存地址来有效地说明这个概念。一旦你摆脱了打破单时钟的想法,你就可以一次从一个线程的角度查看操作,此时你的示例归结为简单的线程干扰。
        • 虽然您编写的所有内容都是正确的,但我认为这对 OP 学习编写安全的 Java 程序的目标没有帮助。 Java 虚拟机有自己的内部内存模型,可以将程序与底层平台的细微差别隔离开来。我给出的例子是他实际上会在编写不正确的 java 程序中遇到的问题,尽管正如你所说的那样,它并不严格符合内存一致性问题的计算机工程定义。
        • 我建议你检查一下你对JVM规范的理解。 JVM 不会隐藏我用作示例的“细微差别”。我要说的是,你的两个例子都是线程干扰。根据定义,MCE必须涉及底层平台。
        • 对于任何在互联网上争论一个星期的线程是值得的 - 这个例子是 Sun/Oracle 教程中 OP 在他的问题中提到的内存一致性问题的例子。因此,我尝试用更多信息对其进行补充,以使他更清楚这两个示例之间的区别。如果原文章有误,我想我们应该让 Oracle 知道! :)
        【解决方案5】:

        1.线程干扰

        class Counter {
            private int c = 0;
        
            public void increment() {
                c++;
            }
        
            public void decrement() {
                c--;
            }
        
            public int value() {
                return c;
            }
        
        }
        

        假设有两个线程 Thread-A 和 Thread-B 工作在 相同的计数器实例。假设 Thread-A 调用 increment() ,并且在 同时 Thread-B 调用 decrement() 。线程 A 读取值 c 并将其增加 1 。同时 Thread-B 读取值 ( 这是 0 因为增量值尚未由 Thread-A) 设置, 递减它并将其设置为 -1 。现在 Thread-A 将值设置为 1 。

        2。内存一致性错误

        当不同的线程有内存一致性错误时发生 共享数据的不一致视图。在上面的类计数器中, 假设有两个线程在同一个计数器实例上工作, 调用 increment 方法将计数器的值增加 1 。这里 不能保证一个线程所做的更改对 另一个。

        更多信息请访问this

        【讨论】:

          【解决方案6】:

          首先,请注意,您的来源并不是学习您想要学习的内容的最佳场所。你会很好地阅读@blucz 的答案(以及他的一般答案)中的论文,即使它超出了 Java 的范围。 Oracle Trails 本身并没有,但是它们简化了问题并掩盖了它们,因此您可能会发现您不了解刚刚学到的内容,或者它是否有用以及有多少。

          现在,尝试主要在 Java 上下文中回答。

          线程干扰

          发生在线程操作交错,即混合时。我们需要两个执行器(线程)和共享数据(干扰的地方)。 图片由 Daniel Stori 提供,来自 turnoff.us 网站:

          在图像中,您可以看到 GNU/Linux 进程中的两个线程可以相互干扰。 Java 线程本质上是指向本机线程的 Java 对象,如果它们对相同的数据进行操作,它们也会相互干扰(比如这里的“Rick”弄乱了他弟弟的数据 - 绘图)。

          内存一致性错误 - MCE

          这里的关键点是内存可见性、happens-before 和 - 由@blucz、硬件提出。

          MCE - 显然 - 内存变得不一致的情况。这实际上是人类的一个术语 - 对于计算机来说,内存始终是一致的(除非它被破坏了)。 “不一致”是人类“看到”的东西,因为他们不明白到底发生了什么,并期待着别的东西。 “为什么是1?应该是2?!”

          这种“感知的不一致性”,这种“差距”,与记忆可见性有关,即不同的线程在看到什么> 记忆。因此这些线程操作。 你看,当我们对代码进行推理时(尤其是在考虑如何逐行执行时),从内存读取和写入内存是线性的......实际上它们不是。特别是当涉及线程时。因此,您阅读的教程为您提供了一个计数器由两个线程递增的示例,以及线程 2 如何读取与线程 1 相同的值。内存不一致的实际原因可能是由于 javac、JIT 或硬件对您的代码进行了优化内存一致性模型(即 CPU 人员为加快 CPU 速度并提高其效率所做的事情)。这些优化包括有先见之明的存储、分支预测,现在您可能将它们视为重新排序代码,以便最终运行更快并使用/浪费更少的 CPU 周期。但是,为了确保优化不会失控(或过分),我们做出了一些保证。这些保证形成了“happen-before”的关系,我们可以看出在此时之前和之后,事情是“happened-before”。想象一下,你举办了一个派对并记住,汤姆在苏西之前来到这里,因为你知道罗布在汤姆之后和苏西之前来了。 Rob 是你用来在 Tom/Suzie 进来之前形成“happens-before”关系的事件。

          https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/package-summary.html#MemoryVisibility

          上面的链接告诉您更多关于内存可见性以及在 Java 中建立之前发生关系的内容。这并不奇怪,但是:

          1. 同步
          2. 启动线程
          3. 加入话题
          4. volatile 关键字告诉您写入发生在后续读取之前,也就是说,写入之后的读取不会被重新排序为“之前”写入,因为这会破坏“发生之前”的关系。

          既然涉及到内存,硬件就必不可少。你的平台有它自己的规则,虽然 JVM 试图通过使所有平台的行为相似来使它们通用,但仅此一点就意味着在平台 A 上的内存障碍将比在平台 B 上更多。

          您的问题

          内存一致性错误和线程干扰有什么区别? MCE 是关于内存对编程线程的可见性,并且在读取和写入之间没有发生之前的关系,因此在人类认为“应该”和什么之间存在差距“实际上是”。

          线程干扰是指线程操作重叠、混合、交错和接触共享数据,将其拧入进程中,可能导致线程A的漂亮绘图被线程B破坏。干扰通常标志着一个关键部分,即为什么同步有效。

          使用同步来避免它们有何不同?

          另请阅读有关瘦锁、胖锁和线程争用的信息。 避免线程干扰的同步只使一个线程访问临界区,而其他线程被阻塞(代价高昂,线程争用)。当涉及到 MCE 同步建立发生之前,当涉及到锁定和解锁互斥锁时,请参阅前面的 java.util.concurrent 包描述链接。

          例如:参见前面的两个部分。

          【讨论】:

            猜你喜欢
            • 2011-02-15
            • 2014-08-20
            • 2019-12-16
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 1970-01-01
            • 2017-10-06
            • 1970-01-01
            相关资源
            最近更新 更多