内存一致性错误和线程干扰有什么区别?使用同步避免它们的方式有何不同或相同?请举例说明。我在Sun Java教程中没有理解清楚。有没有阅读材料可以帮助我纯粹地从Java的角度理解这个问题。
内存一致性错误和线程干扰有什么区别?使用同步避免它们的方式有何不同或相同?请举例说明。我在Sun Java教程中没有理解清楚。有没有阅读材料可以帮助我纯粹地从Java的角度理解这个问题。
内存一致性错误不仅限于Java程序——多CPU系统上的共享内存行为细节高度依赖于体系结构,更糟糕的是,在最初针对多处理器设计的体系结构(例如POWER和SPARC)中,x86(大多数人今天学习编码的地方)与程序员友好的语义相比要简单得多,因此大多数人实际上不习惯考虑内存访问语义。
我举个常见例子说明内存一致性错误可能会给你带来麻烦。假设在这个例子中,x
的初始值为 3。几乎所有体系结构都保证,如果一个 CPU 执行以下代码:
STORE 4 -> x // x is a memory address
STORE 5 -> x
另一个CPU执行
LOAD x
LOAD x
从其两个LOAD
指令的视角来看,将会看到3,3
,3,4
,4,4
,4,5
或5,5
。基本上,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位整数内存位置并从另一个线程中安全地读取它,除非显式同步。很荒谬,对吗?线程干扰要简单得多。基本思想是Java不保证Java代码的单个语句原子执行。例如,增加一个值需要读取该值,增加它,然后再次存储它。所以你可以有int x = 1
在两个线程执行x++
之后,x
可以变成2
或3
,这取决于低层代码交错的方式(在这里工作的低层抽象代码可能看起来像LOAD x, INCREMENT, STORE x
)。这里的基本思想是Java代码被分解成更小的原子片段,除非使用同步原语,否则您无法假设它们如何交错。有关更多信息,请查看这篇文章。它很冗长、单调,而且是由一个臭名昭著的混蛋撰写的,但嘿,它也相当不错。另请参阅这个(或者只需搜索“双重检查锁定已损坏”)。这些内存重排序问题在几年前许多C++/java程序员尝试以他们的单例初始化变得过于聪明时露出了丑陋的头。1. 线程干扰
class Counter {
private int c = 0;
public void increment() {
c++;
}
public void decrement() {
c--;
}
public int value() {
return c;
}
}
内存一致性问题通常表现为破碎的发生前关系。
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也可以解决问题,但可能会让其他程序员感到奇怪,因为他们不知道为什么要这样做,并且在面对使用反射的绑定框架和持久性框架等问题时也可能会出现问题。)
--
线程交错是指两个线程同时操作一个对象,导致看到不一致的状态。
我们有一个PurchaseOrder对象,其中包含itemQuantity和itemPrice属性,自动逻辑生成发票总额。
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是首选方法。
https://docs.oracle.com/javase/8/docs/api/java/util/concurrent/package-summary.html#MemoryVisibility
上面的链接告诉您更多关于内存可见性以及在Java中建立happens-before关系的内容。这并不会让人感到惊讶,但是:
由于所有这些都涉及内存,因此硬件至关重要。您的平台有自己的规则,虚拟机试图通过使所有平台行为类似来使它们通用,但仅仅这一点就意味着在平台A上将有比平台B上更多的内存屏障。
线程干扰是指线程操作相互重叠、混在一起、交错和触及共享数据,从而在过程中损坏它,这可能导致线程A的良好绘图被线程B破坏。由于干扰具有危害性,通常标记临界区域,因此同步工作。内存一致性错误和线程干扰之间有什么区别? MCE是关于程序线程对内存的可见性,而没有在读写之间建立happens-before关系,因此在人们认为“应该是”的内容与“实际上是”的内容之间存在差距。