沧海一粟

天下事有难易乎?为之,则难者亦易矣;不为,则易者亦难矣。

0%

Java内存模型

(1) 内存模型

在多核系统中,处理器一般有一层或者多层的缓存,这些的缓存通过加速数据访问(因为数据距离处理器更近)和降低共享内存在总线上的通讯(因为本地缓存能够满足许多内存操作)来提高CPU性能。缓存能够大大提升性能,但是它们也带来了许多挑战。例如,当两个CPU同时检查相同的内存地址时会发生什么?在什么样的条件下它们会看到相同的值?

在处理器层面上,内存模型定义了一个充要条件,”让当前的处理器可以看到其他处理器写入到内存的数据”以及”其他处理器可以看到当前处理器写入到内存的数据”。有些处理器有很强的内存模型(strong memory model),能够让所有的处理器在任何时候任何指定的内存地址上都可以看到完全相同的值。而另外一些处理器则有较弱的内存模型(weaker memory model),在这种处理器中,必须使用内存屏障(一种特殊的指令)来刷新本地处理器缓存并使本地处理器缓存无效,目的是为了让当前处理器能够看到其他处理器的写操作或者让其他处理器能看到当前处理器的写操作。这些内存屏障通常在lock和unlock操作的时候完成。内存屏障在高级语言中对程序员是不可见的。

由于减少了对内存屏障的需求,有时为强内存模型编写程序可能会更容易。但是,即使在某些最强大的内存模型上,也经常需要使用内存屏障。它们的放置经常违反直觉。处理器设计的最新趋势鼓励使用较弱的内存模型,因为它们对缓存一致性的放松使得跨多个处理器的可伸缩性和内存量更大。

在强内存模型下,有时候编写程序可能会更容易,因为减少了对内存屏障的依赖。但是即使在一些最强的内存模型下,内存屏障仍然是必须的。设置内存屏障往往与我们的直觉并不一致。近来处理器设计的趋势更倾向于弱的内存模型,因为弱内存模型削弱了缓存一致性,所以在多处理器平台和更大容量的内存下可以实现更好的可伸缩性

“一个线程的写操作对其他线程可见”这个问题是因为编译器对代码进行重排序导致的。例如,只要代码移动不会改变程序的语义,当编译器认为程序中移动一个写操作到后面会更有效的时候,编译器就会对代码进行移动。如果编译器推迟执行一个操作,其他线程可能在这个操作执行完之前都不会看到该操作的结果,这反映了缓存的影响。

此外,写入内存的操作能够被移动到程序里更前的时候。在这种情况下,其他的线程在程序中可能看到一个比它实际发生更早的写操作。所有的这些灵活性的设计是为了通过给编译器,运行时或硬件灵活性使其能在最佳顺序的情况下来执行操作。在内存模型的限定之内,我们能够获取到更高的性能。

In multiprocessor systems, processors generally have one or more layers of memory cache, which improves performance both by speeding access to data (because the data is closer to the processor) and reducing traffic on the shared memory bus (because many memory operations can be satisfied by local caches.) Memory caches can improve performance tremendously, but they present a host of new challenges. What, for example, happens when two processors examine the same memory location at the same time? Under what conditions will they see the same value?

At the processor level, a memory model defines necessary and sufficient conditions for knowing that writes to memory by other processors are visible to the current processor, and writes by the current processor are visible to other processors. Some processors exhibit a strong memory model, where all processors see exactly the same value for any given memory location at all times. Other processors exhibit a weaker memory model, where special instructions, called memory barriers, are required to flush or invalidate the local processor cache in order to see writes made by other processors or make writes by this processor visible to others. These memory barriers are usually performed when lock and unlock actions are taken; they are invisible to programmers in a high level language.

It can sometimes be easier to write programs for strong memory models, because of the reduced need for memory barriers. However, even on some of the strongest memory models, memory barriers are often necessary; quite frequently their placement is counterintuitive. Recent trends in processor design have encouraged weaker memory models, because the relaxations they make for cache consistency allow for greater scalability across multiple processors and larger amounts of memory.

The issue of when a write becomes visible to another thread is compounded by the compiler’s reordering of code. For example, the compiler might decide that it is more efficient to move a write operation later in the program; as long as this code motion does not change the program’s semantics, it is free to do so. If a compiler defers an operation, another thread will not see it until it is performed; this mirrors the effect of caching.

Moreover, writes to memory can be moved earlier in a program; in this case, other threads might see a write before it actually “occurs” in the program. All of this flexibility is by design – by giving the compiler, runtime, or hardware the flexibility to execute operations in the optimal order, within the bounds of the memory model, we can achieve higher performance.

(2) Java内存模型

(2.1) 什么是Java内存模型

本质上可以理解为 Java 内存模型规范了 JVM 如何提供按需禁用缓存和编译优化的方法。
具体来说,这些方法包括 volatile、synchronized 和 final 三个关键字,以及六项 Happens-Before 规则。

(2.2) Java内存模型解决了什么问题

Java内存模型解决了Java并发编程里的可见性有序性问题

(2.3) Java内存模型的具体规则

volatile

告诉编译器,对这个变量的读写,不能使用 CPU 缓存,必须从内存中读取或者写入。
解决可见性问题

synchronized

同步
解决原子性问题

final

不可变

Happens-Before 规则

(1)程序的顺序性规则

在一个线程中,按照程序顺序,前面的操作 Happens-Before 于后续的任意操作。

在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。准确地说,应该是控制流顺序而不是程序代码顺序,因为要考虑分支、循环等结构。

(2)volatile 变量规则

对一个 volatile 变量的写操作, Happens-Before 于后续对这个 volatile 变量的读操作。
对一个volatile变量的写操作先行发生于后面对这个变量的读操作,这里的”后面”同样是指时间上的先后顺序。

1
2
3
4
5
6
7
8
9
10
11
12
13
class VolatileExample {
int x = 0;
volatile boolean v = false;
public void writer() {
x = 42;
v = true;
}
public void reader() {
if (v == true) {
// 这里x会是多少呢?
}
}
}

(3)传递性

如果 A Happens-Before B,且 B Happens-Before C,那么 A Happens-Before C。

(4)管程中锁的规则

对一个锁的解锁 Happens-Before 于后续对这个锁的加锁。
一个unlock操作先行发生于后面对同一个锁的lock操作。这里必须强调的是同一个锁,而”后面”是指时间上的先后顺序。

管程是一种通用的同步原语,在 Java 中指的就是 synchronized,synchronized 是 Java 里对管程的实现。

1
2
3
4
5
6
7

synchronized (this) { //此处自动加锁
// x是共享变量,初始值=10
if (this.x < 12) {
this.x = 12;
}
} //此处自动解锁

(5)线程 start()

关于线程启动的
主线程 A 启动子线程 B 后,子线程 B 能够看到主线程在启动子线程 B 前的操作。
Thread对象的start()方法先行发生于此线程的每一个动作。

1
2
3
4
5
6
7
8
9
Thread B = new Thread(()->{
// 主线程调用B.start()之前
// 所有对共享变量的修改,此处皆可见
// 此例中,var==77
});
// 此处对共享变量var修改
var = 77;
// 主线程启动子线程
B.start();

(6)线程 join() 规则

关于线程等待的
主线程 A 等待子线程 B 完成(主线程 A 通过调用子线程 B 的 join() 方法实现),当子线程 B 完成后(主线程 A 中 join() 方法返回),主线程能够看到子线程的操作。

线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过Thread.join()方法结束、Thread.isAlive()的返回值等手段检测到线程已经终止执行。

1
2
3
4
5
6
7
8
9
10
11
12
Thread B = new Thread(()->{
// 此处对共享变量var修改
var = 66;
});
// 例如此处对共享变量修改,
// 则这个修改结果对线程B可见
// 主线程启动子线程
B.start();
B.join()
// 子线程所有对共享变量的修改
// 在主线程调用B.join()之后皆可见
// 此例中,var==66

再送一个
对象终结规则:一个对象的初始化完成(构造函数执行结束)先行发生于它的finalize()方法的开始。

References

[1] 《深入理解Java虚拟机》 周志明
[2] 02 | Java内存模型:看Java如何解决可见性和有序性问题
[3] Java内存模型FAQ
[4] JSR 133 (Java Memory Model) FAQ
[5] jsr133.pdf