【发布时间】:2010-09-03 00:42:59
【问题描述】:
内存一致性错误和线程干扰有什么区别? 使用同步来避免它们有何不同?请举例说明。我无法从 sun Java 教程中得到这个。任何阅读材料的建议以纯粹在 java 的上下文中理解这一点都会有所帮助。
【问题讨论】:
标签: java multithreading concurrency
内存一致性错误和线程干扰有什么区别? 使用同步来避免它们有何不同?请举例说明。我无法从 sun Java 教程中得到这个。任何阅读材料的建议以纯粹在 java 的上下文中理解这一点都会有所帮助。
【问题讨论】:
标签: java multithreading concurrency
内存一致性错误不能纯粹在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,4、4,4、4,5 或5,5 从它的两个LOAD 指令的角度来看。基本上,CPU 保证从所有 CPU 的角度来看,对单个内存位置的写入顺序保持不变,即使允许其他 CPU 知道每个写入的确切时间有所不同。
CPU 彼此不同的地方往往在于它们对涉及不同内存地址的LOAD 和STORE 操作做出的保证。假设对于这个例子,x 和 y 的初始值都是 4。
STORE 5 -> x // x is a memory address
STORE 5 -> y // y is a different memory address
然后另一个 CPU 执行
LOAD x
LOAD y
在这个例子中,在某些架构上,第二个线程可以看到4,4、5,5、4,5、或5,4。哎哟!
大多数架构以 32 位或 64 位字的粒度处理内存 - 这意味着在 32 位 POWER/SPARC 机器上,您无法更新 64 位整数内存位置并安全地从另一个没有显式同步的线程ever。傻了吧?
线程干扰要简单得多。基本思想是java不保证java代码的单个语句原子执行。例如,增加一个值需要读取该值,增加它,然后再次存储它。因此,在两个线程执行 x++ 之后,您可以拥有 int x = 1,x 最终可能为 2 或 3,具体取决于较低级别代码的交错方式(这里工作的较低级别抽象代码大概看起来比如LOAD x, INCREMENT, STORE x)。这里的基本思想是,java 代码被分解成更小的原子片段,除非你明确使用同步原语,否则你不能假设它们是如何交错的。
有关更多信息,请查看this 论文。它又长又干,是一个臭名昭著的混蛋写的,但是嘿,它也很不错。还可以查看this(或者只是谷歌“双重检查锁定被破坏”)。这些内存重新排序问题引起了许多 C++/java 程序员的丑陋,他们在几年前试图让他们的单例初始化变得有点过于聪明。
【讨论】:
线程干扰是关于线程覆盖彼此的语句(例如,线程 A 增加一个计数器而线程 B 减少它同时),导致计数器的实际值不可预测的情况.您可以通过强制独占访问来避免它们,一次一个线程。
另一方面,内存不一致与可见性有关。线程 A 可能会增加 counter,但随后线程 B 可能不知道此更改尚未,因此它可能会读取一些先前的值。你可以通过建立一个happens-before关系来避免它们,即
只是保证一个特定语句写入的内存对另一个特定语句可见。(每Oracle)
【讨论】:
要阅读的文章是 Adve 和 Boehm 在 2010 年 8 月的第一卷中的“内存模型:重新思考并行语言和硬件的案例”。 53 第 8 期 ACM 通讯。计算机机械协会会员 (http://www.acm.org) 可在线获取此信息。这涉及一般问题,还讨论了 Java 内存模型。
有关 Java 内存模型的更多信息,请参阅http://www.cs.umd.edu/~pugh/java/memoryModel/
【讨论】:
内存一致性问题通常表现为先发制人的关系破裂。
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 通常是新程序的首选方法。
【讨论】:
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。
【讨论】:
首先,请注意,您的来源并不是学习您想要学习的内容的最佳场所。你会很好地阅读@blucz 的答案(以及他的一般答案)中的论文,即使它超出了 Java 的范围。 Oracle Trails 本身并没有坏,但是它们简化了问题并掩盖了它们,因此您可能会发现您不了解刚刚学到的内容,或者它是否有用以及有多少。
现在,尝试主要在 Java 上下文中回答。
发生在线程操作交错,即混合时。我们需要两个执行器(线程)和共享数据(干扰的地方)。 图片由 Daniel Stori 提供,来自 turnoff.us 网站:
在图像中,您可以看到 GNU/Linux 进程中的两个线程可以相互干扰。 Java 线程本质上是指向本机线程的 Java 对象,如果它们对相同的数据进行操作,它们也会相互干扰(比如这里的“Rick”弄乱了他弟弟的数据 - 绘图)。
这里的关键点是内存可见性、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 中建立之前发生关系的内容。这并不奇怪,但是:
既然涉及到内存,硬件就必不可少。你的平台有它自己的规则,虽然 JVM 试图通过使所有平台的行为相似来使它们通用,但仅此一点就意味着在平台 A 上的内存障碍将比在平台 B 上更多。
内存一致性错误和线程干扰有什么区别? MCE 是关于内存对编程线程的可见性,并且在读取和写入之间没有发生之前的关系,因此在人类认为“应该”和什么之间存在差距“实际上是”。
线程干扰是指线程操作重叠、混合、交错和接触共享数据,将其拧入进程中,可能导致线程A的漂亮绘图被线程B破坏。干扰通常标志着一个关键部分,即为什么同步有效。
使用同步来避免它们有何不同?
另请阅读有关瘦锁、胖锁和线程争用的信息。 避免线程干扰的同步只使一个线程访问临界区,而其他线程被阻塞(代价高昂,线程争用)。当涉及到 MCE 同步建立发生之前,当涉及到锁定和解锁互斥锁时,请参阅前面的 java.util.concurrent 包描述链接。
例如:参见前面的两个部分。
【讨论】: