内存一致性错误与线程干扰

时间:2010-09-03 00:42:59

标签: java multithreading concurrency

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

6 个答案:

答案 0 :(得分:16)

纯粹在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,3指令的角度看3,44,44,55,5LOAD。基本上,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或OR 5,4。哎哟!

大多数体系结构以32位或64位字的粒度处理内存 - 这意味着在32位POWER / SPARC计算机上,您无法更新64位整数内存位置并安全地从另一个位置读取它没有显式同步的线程永远。高飞,是吗?

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

有关详细信息,请查看this论文。它是漫长而干燥的,由一个臭名昭着的混蛋写的,但是,嘿,这也很不错。还可以查看this(或只是谷歌“双重检查锁定已损坏”)。这些内存重新排序问题为许多C ++ / java程序员带来了丑陋的头脑,这些程序员几年前试图让他们的单例初始化变得有点过于聪明。

答案 1 :(得分:4)

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

另一方面,内存不一致是关于可见性的。线程A可以递增counter,但是线程B可能不知道此更改尚未,因此它可能会读取一些先前的值。你通过建立一个发生在之前的关系来避免它们,这是

  

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

答案 2 :(得分:2)

关于这一点的文章是“记忆模型:重新思考平行语言和硬件的案例”,Adve和Boehm在2010年8月的一卷。 53号ACM通讯第8期。这可以在线获得计算机机械协会会员(http://www.acm.org)。这一般涉及问题,还讨论了Java内存模型。

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

答案 3 :(得分:0)

内存一致性问题通常表现为在关系之前发生故障。

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通常是新程序的首选方法。

答案 4 :(得分:0)

<强> 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()。 Thread-A读取值c   并将其增加1。同时Thread-B读取值(   这是0,因为Thread-A尚未设置递增的值,   递减它并将其设置为-1。现在,Thread-A将值设置为1。

<强> 2。内存一致性错误

  

内存一致性错误在不同的线程出现时发生   共享数据的不一致视图。在上面的班级柜台,   可以说有两个线程在同一个计数器实例上工作,   调用increment方法将计数器的值增加1。这里   不能保证一个线程所做的更改是可见的   另一个。

更多访问this

答案 5 :(得分:0)

首先,请注意,您的来源不是了解您尝试学习的最佳位置。你会很好地阅读@blucz的答案(以及他的答案)中的论文,即使它超出了Java的范围。 Oracle Trails本身并不是,但它们可以简化问题并掩盖它们,因此您可能会发现自己并不了解自己刚刚学到的东西或是否已经#&#&# 39;有用与否有多少。

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

线程干扰

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

Daniel Stori, turnoff.us

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

内存一致性错误 - MCE

这里的关键点是内存可见性,发生在之前和 - 由@blucz,硬件提出。

MCE显然是情况,内存变得不一致。这实际上是人类的术语 - 对于计算机而言,记忆在任何时候都是一致的(除非它被破坏)。 &#34;不一致&#34;是人类正在看到的东西#34;因为他们不了解究竟发生了什么,并期待别的东西。 &#34;为什么是1?它应该是2?!&#34;

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

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

上面的链接告诉您更多关于内存可见性以及在Java之前建立的关系。这不会让人感到意外,但是:

  1. 同步确实
  2. 开始一个主题
  3. 加入主题
  4. volatile关键字告诉您在后续读取之前发生写入,也就是说,写入后写入将不会被重新排序为&#34;之前&#34;写道,因为那会破坏&#34;发生 - 之前&#34;关系。
  5. 由于所有触及内存的东西,硬件都是必不可少的。您的平台拥有它自己的规则,而JVM试图通过使所有平台的行为相似来使它们具有通用性,仅仅这一点意味着在平台A上存在比在平台B上更多的内存障碍。

    您的问题

      

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

    线程干扰是关于线程操作重叠,混合,交错和触摸共享数据,将其拧入过程中,这可能导致线程A具有被线程B破坏的良好绘图。干扰有害通常标记为关键部分,这是为什么同步有效。

      

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

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

    例如:见前两节。