The Java Memory Model by Jeremy Manson, William Pugh, Sarita V. Adve 笔记
论文网址:
一篇有些年头的论文。
ABSTRACT:
The new Java model provides a simple interface for correctly synchronized programs– it guarantees sequential consistency to data-race-free programs.
INTRO:
对于程序员:一坨代码会出那些可能的结果?
对于其他:编译器怎么优化转换字节码?JVM怎么优化转换机器码?硬件怎么优化并执行机器码?
1.1 BG:
最易于理解的模型是顺序一致性(sequential consistency),它规定内存操作必须看似按单一全局顺序逐一执行;给定线程的操作在此全局顺序中必须与程序中的顺序(称为程序顺序,program order)一致。
例如,如果线程 A 的代码是:
x = 1; // 操作 1 y = 2; // 操作 2
那么在所有线程观察到的全局执行顺序中,x = 1 必须在 y = 2 之前(当然实际在执行可以优化,但是从结果来说,绝对不能反过来)。
那么其实可以发现,在java中,单线程的程序当然是顺序一致的了,比如
int x = 1; // 操作 A
int y = x + 2; // 操作 B
System.out.println(y); // 操作 C
上面这个东西执行多少遍都只能是print出来3(当然程序大概率会被优化成直接print(3))。
这里需要注意一点,所谓顺序一致性,针对的是程序的可见结果,也就是“真正去使用内存的某个值的时候的一致性”。
比如
void foo(){
int x = 1; // 操作 A
int y = x + 2; // 操作 B
}
如果一段程序只有这两个操作(只存不用),那就可以随便换,反正对于程序执行的结果没有改变。
好像听起来不错,但是问题来了:
上面这玩意儿在顺序一致性下,绝对不会出现这个情况,有人可能觉得是好事,但是问题是,如果这两个线程完全不相干,那重排序也没啥问题,也就是说我们的优化的可能性被大大减少了。
提出:
无数据竞争模型(data-race-free models)
这里的意思是,我们对于程序的某一些部分,需要data-race-free,java会给我们方法来保证这一点(比如synchronize,volatile那些操作,我们加上即可),在其他地方,我们可能根本不需要race-free,就可以让jvm放心去优化重排序我们的程序(这里就会让我们的程序出现上面的r2==2, r1==1出现)。
1.2 The Java Memory Model
如果一个模型对不正确程序的语义未作规定,它可能允许违反 Java 程序所需的安全性和安全性属性,(所以我们需要一个规定)。
这里的意思是,比如我们写了一个代码,出现了A,B,C三种结果,这个C的结果是我们不想要的(是不正确的),如果我们缺少了相关的“语义”来明确C这个结果到底是“怎么样的不正确”,那么java就会出现缺陷。我们需要明确指出C到底可以归结于哪一类的“不正确”。(JMM并不会直接给出C的结果是哪一种“不正确”,JMM会给出一组规则)。
“在早期的宽松模型中,这类执行要么被允许,要么仅通过强制传统的控制和数据依赖来禁止。我们表明,后一种方法限制了标准编译器优化,对 Java 来说是不可行的。”
“修订后的 Java 模型基于一种新技术,该技术在禁止必要执行的同时允许标准优化。我们的技术通过迭代构建合法执行。在每次迭代中,它提交一组内存操作;只有当这些操作出现在某个行为良好的执行中(该执行还包含之前迭代中提交的操作)时,操作才可以被提交。通过对“行为良好的执行”进行仔细定义,确保禁止适当的执行并允许标准编译器转换。据我们所知,这是唯一具有这种属性的模型。”
2. REQUIREMENTS FOR THE JAVA MEMORY MODEL
Java 的内存模型是一个宽松模型。
我们希望我们的新的JMM能够做到一些事情……
2.1 Correctly Synchronized Programs
此处我们的JMM定义什么叫做“正确同步的程序(就是不会发生data race的程序)”,以及对于这种程序的正确的“语义”。
1. Conflicting Accesses: 两个对于同一个共享区域的access,且至少其中一个是写
2. Synchronization Actions: include locks, unlocks, reads of volatile variables, and writes to volatile variables.
3. Synchronization Order: 这是一种对于 synchronization actions的全序关系。为了能够正确地定义data-race-free程序,我们只考虑那些 1. 与program order一致的SO,2. 一个对于volatile变量v的读返回的值,就是在SO中这一次读的刚好上一次volatile写的值。这个第二个要求再剩余的paper中,被imposed为一个SO一致性需求。
4. Synchronizes-With Order: 对于两个操作,x和y,x --sw-> y == x synchronizes-with y
An unlock action on monitor m synchronizes-with all subsequent lock actions on m that were performed by any thread, where subsequent is defined accord ing to the synchronization order. Similarly, a write to a volatile variable v synchronizes-with all subse quent reads of v by any thread. There are additional synchronized-with edges in Java [20] that are not dis cussed here for brevity.
5. Happens-Before Order: PO和SW的传递闭包
这里给一个例子:
class Example { volatile int x = 0; int y = 0; final Object lock = new Object(); void thread1() { x = 1; // 操作 A: volatile 写 synchronized (lock) { y = 2; // 操作 B: 锁内写 } // 操作 C: 解锁 } void thread2() { synchronized (lock) { // 操作 D: 加锁 int r1 = y; // 操作 E: 锁内读 } int r2 = x; // 操作 F: volatile 读 } }
T1 | T2
x=1 A | lock D
lock | r1=y E
y=2 B| unlock
unlock C| r2=x F
这里我们假定,T1先拿到了锁。
首先是PO:
线程1:A --po-> B --po-> C, 线程2:D --po-> E --po-> F
然后是SW:
首先对于volatile的读写,如果x=1写入了,r2=1,那么这里就有一个SW的关系
x, r2 = (1, 1) : x--sw->r2
对于lock
T1先执行,所以C--sw->D
连起来的话,就是A-->B-->C-->D-->E-->F
Data Race:
不在HB里面的conflict access
Correctly Synchronized or Data-Race-Free Program:
该程序的所有顺序一致性(sequential consistency)执行都没有数据竞争(data races)
JMM的责任:为data-race free程序提供顺序一致性的能力
程序员的责任:好好想想哪里可能会出现data races,然后加上这些JMM提供的东西来让他们做到顺序一致性,然后避免数据竞争
根据上面的语义,该程序的所有顺序一致性执行(例如1,2,3,4)的conflict access(例如2-->3),没有出现在HB里面(跨线程,没有SA,当然没有SO;没有SO,当然没有SW;没有SW,当然不在HB里面)。按照定义,这就是data races,所以这个程序就不是Correctly Synchronized or Data-Race-Free Program。
至此,我们定义了什么是“正确同步”的程序。
2.2 Out-of-Thin-Air Guarantees for Incorrect Programs
这里我们希望新的JMM做到:
给未正确编写的代码提供清晰且明确的语义,说明其行为方式,但不得显著影响当前的编译器和硬件
自我证明:线程 1 可能推测性地将值 42 写入 y,这会允许线程 2 读取 y 的值为 42 并将其写入 x,从而允许线程 1 读取 x 的值为 42(线程1指令重排序了),并证明其最初对 y 的推测性写入 42 是合理的。
3. HAPPENS-BEFORE MEMORY MODEL
先讲讲这个HB模型,比较简单
要求:
1.
存在一个针对同步动作(synchronization actions)的同步顺序(synchronization order),同步动作在匹配的动作之间诱导 synchronizes-with 边,synchronizes-with 边与程序顺序(program order)的传递闭包(transitive closure)共同形成一个称为 happens-before 顺序的顺序(如第 2.1 节所述)。
就是上面那个照搬过来。
2.
对于每个线程 t,t 在执行中执行的动作与 t 在程序顺序(program order)下独立执行时生成的动作相同,给定 t 在执行中读取操作看到的值
我们可以想象有一个人一直在给我们的单线程程序写入数据,在这种情况下要满足PO。
3.
一个称为 happens-before 一致性(happens-before consistency)的规则决定了非 volatile 读取能看到的值。
4.
一个称为同步顺序一致性(synchronization order consistency)的规则决定了 volatile 读取能看到的值。
HB:1. 如果在HB中,读在写之前,那就读不到
2. 如果HB中,读和写之间穿插了一个写,那就读不到更早的那一次写
对于除此之外的non-volatile读,全部能读到(不在HB里面随便读,在HB里面只读上一次)
SO:
- 同步顺序与程序顺序一致。
- 对于volatile变量v的每次读操作r,会看到在同步顺序中位于它之前的最后一次对v的写操作。
如果要进入分支,那就是ready=true,那就说明T1的ready写一定在T2读之前,此处就有一条HB边。我们再连上PO序,就能保证r1=x就是刚刚写的值。
如果不是volatile,就不能保证。事实上,就有可能x=1跟ready=true重排序,确实没法保证。
4. CAUSALITY(因果)
HB的问题:
1.
不可接受的行为。注意,我们希望禁止的图2中的行为与happens-before内存模型是一致的。如果两个写操作都写入了值42,并且两个读操作都看到了这些写操作,那么两个读操作看到的值都是它们被允许看到的。
这个怎么理解呢?警告,上面的HB对于非volatile操作的约束,是filter,也就是过滤类型的。回顾一下:
1)如果在HB中,读在写之前,那就读不到
2) 如果HB中,读和写之间穿插了一个写,那就读不到更早的那一次写
也就是说这个filter对于图2的程序没有任何作用,因为没有HB。
而且还有上面的第二条,我们可以想象有一个人一直在给我们的单线程程序写入数据,在这种情况下要满足PO。
这就说明了,如果不在HB中,那确实可能出现一个人跑出来往我们的程序的共享变量写入42。然后确实也满足了PO,都是顺序执行下来的。
所以说确实有可能是这样的,我们不希望出现这种结果,但是我们的HB模型啥也帮不了我们。
2.
我们希望“correctly synchronized programs”有顺序一致性的语义,HB模型做不到。
回顾一下定义:
该程序的所有顺序一致性(sequential consistency)执行都没有数据竞争(data races)。
为了分析正确同步,我们给与代码顺序执行的能力。如果上面的程序已经在顺序一致性下执行了,那么每一个操作都出现在PO,那么我们对于x和y的写入是不可能发生的(无非就是r1=x;先执行还是r2=y先执行,那就不可能进入if分支),也就是说这个程序压根就没有写入,哪来的conflict access?哪来的data races?所以就是正确同步的。
那么问题来了,这个“正确同步”的代码,顺序一致的结果只可能是0,0。但是HB模型完全无法保证,r1 == r2 == 42不会出现。
在现实中, 如果y=42和x=42执行了,那么再r1=x,r2=y,再进入分支,完全有可能。这种错误的执行是我们希望极力避免的,然而这个操作也是不违反HB模型的。
所以说HB模型有缺陷,无法满足我们对于船新JMM的要求。
(注意这里的逻辑:我们在现实中可以构造出一种执行流程,这个执行流程是可能的,我们不希望这个执行流程能通过JMM的审核。假如使用了HB模型作为我们的JMM模型,那么这个执行流程就能成为现实,就能通过审核,这是我们不希望的。)
4.1 Data and Control Dependencies
要怎么去弥补我们希望的语义跟HB模型之间的鸿沟呢?
因果关系(causality)。
非法写操作发生的动作本身是由这些写操作引起的。circular causality
比如图二
实际上我们先执行了y=42,然后t2就会读到42然后x=42,然后r1=42 。所以y先被写入了42,然后r1=42,证明了y确实是42。
如果我们贸然去减少所有的类似的优化,同样会出现问题。对于图五来说,如果我们为了解决循环问题,使用某种方法取消掉了都等于2的可能性,实则帮了倒忙,因为这里确实是希望能出现都是2的。
对于图6来说,编译器能够分析出来某一个值只可能是1,所以就打破了数据依赖,然后重排序然后出现这种情况。
总之,图5、6都是好的结果,我们不希望jmm把这些结果剔除。
4.2 Causality and the Java Memory Model
可接受与不可接受结果之间的一个区别在于,在图5中,我们提早执行的写操作4: b = 2即使在顺序一致的执行中也会发生,访问相同的内存位置并写入相同的值。而在图4中,提早的写操作在任何顺序一致的执行中都不可能写入相同的值(42)。
我们应该考虑,一个动作应该在何时被允许发生。
行为良好的执行(well-behaved execution):在该执行中,写操作已经发生了,然后用该执行来证明合理性。可以迭代构建。
那么什么是行为良好呢?比如“能否在顺序一致下执行”。可能过于宽松。
结论:
如果一个动作的提早执行不依赖于读操作返回的数据竞争(data race)值,那么它不会导致不希望的因果循环。
此处讲讲图五
首先通过initially的HB关系,我们可以得到r1和r2的两个0,然后我们就可以提交b=2了。这就让我们得以在线程二中选择。r3=1是justifying execution(已经在hb里面而没有race),r3=2是execution being justified,也就是刚刚已经提交了的b的值为2。然后我们把a=2提交上去。然后我们再次给r1=a和r2=a进行选择,同样选择已经justified的a=2。所以如此一来,就有了全部等于2的可能性。(所以一个值可以被读入两次,甚至不需要相同)。
5. FORMALSPECIFICATION
This section provides the formal specification of the Java memory model.
5.1 Actions and Executions
1. 动作(Action)是什么?
-
一个线程对共享变量或同步机制的一次原子性操作。
-
动作有各种类型(如普通读写、volatile 读写、锁操作、线程控制等)。
-
特别关注 同步动作,因为它们影响线程间的顺序与可见性。
2. 执行(Execution)由什么组成?
执行 ≈ 某个程序实际运行的一种状态记录。包含:
-
程序顺序(po):线程内的执行顺序(程序本身的顺序)。
-
同步顺序(so):所有同步动作的全局排序(线程间协调)。
-
写可见函数(W):每个读看到哪个写。(注意,这是一个函数,也就是说,给一个读返回一个写的action)
-
写值函数(V):每个写写了什么值。(也是一个函数,给一个写入的操作,返回这个写入的值)
-
同步关系(sw):跨线程同步因果(如 unlock → lock)。
✅ happens-before 和 synchronizes-with 都是由这些组成部分唯一决定的。
3. 特殊动作:为什么单独讨论?
a. External Actions(外部动作)
-
与外部环境交互,如打印、网络 I/O。
-
结果依赖于外部环境,可能被观察到。
-
内存模型只关注结果,不关心参数设置。
b. Thread Divergence Actions(线程发散)
-
表示线程进入纯死循环(不做内存/同步/外部操作)。
-
会导致系统“卡住”或其他线程无法继续执行。
5.2 Definitions
SW:源头是《release》,目的地是《acquire》
5.3 Well-Formed Executions
一个执行 E=⟨P,A,po→,W,V,sw→,hb→⟩ 是良好形成的,需满足以下条件:
- 读取-写入匹配:
- 每个读取对应一个写入,读写的变量一致。
- 易失性(volatile)变量的读写是易失性动作,读写变量的易失性与动作类型一致。
- 同步与程序顺序协调:
- 同步顺序与程序顺序一致,happens-before 关系是有效偏序(自反、传递、反对称)。
- 锁和解锁动作在每个监视器上正确嵌套,确保互斥。
- 线程内一致性:
- 每个线程的动作序列与独立执行时的程序顺序一致。
- 写入值由 V(w) 决定,读取值由 V(W(r))决定,符合内存模型。
- 程序顺序反映线程内语义。
- 同步顺序一致性:
- volatile读取不能在同步顺序上早于其对应的写入。
- 不能有其他写入干扰读取与其对应写入的同步顺序。
- 发生前一致性:
- 读取不能在 happens-before 关系上早于其对应的写入。
- 不能有其他写入干扰读取与其对应写入的 happens-before 关系。
对于上面的第三点:
对于多线程的某一个线程t的执行来说,我们可以想象我们的execution是某一个权威人士,能够提供所有对于共享变量的读和写的值(通过给定的V和W函数),除此之外,t的执行流程从自己的角度来说,就是program-order,也就是跟单线程执行是类似的,顺着执行下来
5.4 Causality Requirements for Executions
凭什么来判断一个执行流程好不好呢?我们检查他提供的动作集合A。如果都可以提交(commit),就well-formed。
我们的commit集合首先是空集。
每次从动作集 A 中选取动作,我们组合成一个Ci(所以Ci也是动作集合),这个然后一定展示一个包含了Ci的Ei,这个Ei一定要满足一些条件。
我们提交的E必须满足什么条件呢?当且仅当:
1.
一系列动作集 C0,C1,… 满足:
- C0=∅(初始为空集)。
-
C i ⊂C i+1 (每个后续集严格包含前一个集)。
-
A=∪(C 0 ,C 1 ,C 2 ,…)(所有动作集的并集等于整个动作集 𝐴 )。 并且, 𝐸 和 ( 𝐶 0 , 𝐶 1 , 𝐶 2 , …
) 满足下面列出的限制条件。
该序列 𝐶 0 , 𝐶 1 , … 可以是有限的,以某个 𝐶 𝑛 = 𝐴 结束。如果 𝐴 是无限集,则序列 𝐶 0 , 𝐶 1 , …可以是无限的,且必须满足所有动作集的并集等于 𝐴 。
2.
一系列良好形成执行 𝐸 1 , 𝐸 2 , … E 1 ,E 2 ,…,其中每个 𝐸 𝑖 = ⟨ 𝑃 , 𝐴 𝑖 , 𝑝 𝑜 𝑖 → , 𝑠 𝑜 𝑖 → , 𝑊 𝑖 , 𝑉 𝑖 , 𝑠 𝑤 𝑖 → , ℎ 𝑏 𝑖 → ⟩ 。
通俗化解释:
比方我大脑一转,想出来一个E,里面有一个A,我觉得我的E是well-formed的,但是JMM会来检查。JMM首先做一个空集,然后从我的A里面提取动作来构建C1,然后使用一个包含了C1的E1,观察这个E1是否满足某些要求。如果满足了,就继续加入A里面的东西到C1来组成C2. 如此操作,直到我的E的A的所有操作都被包含起来。
那么E1一定要满足的是什么要求呢?
1. C i ⊆A i
我们选取的Ei里面的Ai一定包含Ci
2.
hbi ∣ C i = hb ∣ C i
首先我们回忆一下竖杠是啥意思。竖杠的意思是,所有的偏序关系的定义域(偏序关系肯定是左边一个a0,右边一个a1,中间放一个HB。这里就是两个action都能取什么值)。
所以这个的意思是,我们从Ci里面取出来的所有的action,他们首先都在Ei(第一条),然后我们从Ei里面拿到这个hbi,这些action满足这个hbi。这与我上面大脑一转提交的总的执行E里面的hb的Ci那一部分必须是一致的。比如我觉得a=3跟r1=a有一个hb关系,那么我们构建的某一个Ei的时候想出来的一个hbi,也会出现a=3跟r1=a的一个hb关系。
3. soi →|Ci = so → |Ci
同理
4和5
对于5,Wi|Ci−1 = W|Ci−1,可能有些难以理解。
首先回顾W,W是一个函数,给一个读的action,返回写入的那个action
在Ci的写入的值们,必须要跟Ei和E里面是一样的。在Ci-1的读入,需要在Ei和E看到相同的写入。
这怎么理解?比如说Ei里面有一个Ai,Ai包含了Ci,Ci又包含了Ci-1 。
这一条给的要求是,对于Ei里面的Wi,我们只提取那些上一次提交(Ci-1)的读入操作,然后这个Wi对于这些操作会返回写入的action1。而对于我们拍脑袋想出来的那个E的W来说,提供Ci-1的读操作也会返回写入的action2。我们希望action1=action2
为什么我们只关注Ci-1,而不是Ci呢?是这样的,我们在新的一次Ci的提交中,提交了更多的读取操作(Ci-(Ci-1)),对于这些读取操作,我们可能暂时还没有办法提供对应的写入的action,我们一步一步操作,先找到上一次提交的读操作的写入操作。
6,7
也就是说,对于新提交的这些读取操作,r属于Ci-Ci-1:
允许 𝑊 𝑖 ( 𝑟 ) ≠ 𝑊 ( 𝑟 )
也就是说,最后W可以在下一次变卦,但是依然要保证变卦的写是在i-1就提交过了。
第八条:
如果x sswi y影响了新加入的操作,那么这个x ssw y就是比较重要的,所以后续必须保留
9:如果y提交了,跟y扯上了hb关系的外部操作必须全部提交
6. EXAMPLE
为什么在C2提交r2=y=1呢?因为线程一对于y的写入还没有提交呢(无法提供W)。但是这里线程二可以看见初始的值(y=0),而且是hb关系,所有E2就会有r2=y=0
A2-C1 = (r2=y), 所以这里的所有操作必须有hb,然而有hb的只有读入初始值。
所以在E3就不再有上面的限制,就可以读到所有。所以就成功读到y=1.
In E3, by Rule 7, r2 = y can see any conflicting write that occurs in C2
此处怀疑有误,按照rule 7的说法,既然r2 = y是C2提交的,那就应该是W(r2)∈C1,能看到C1里面的提交。
7. OBSERVABLE BEHAVIOR AND NONTERMINATING EXECUTIONS
observable behavior:能观察到的外部操作有限集合
hang:
1. 所有未终止的线程都被阻塞(blocked)
2. 程序可以执行无限数量的动作,但不产生任何外部动作
阻塞线程的动作:
- 如果一个线程无限期阻塞,它的行为只包括阻塞前的动作,以及导致阻塞的那个动作。
- 阻塞后的动作不会被包含,因为线程已经无法继续执行。
if an action y ∈ O, and either x hb → yorx so → y, then x ∈ O.
observable actions的集合O,可大可小,唯一的要求是前面的(hb和so靠前)都在O里面
注意,可观察动作集合并不局限于仅包含外部动作。相反,只有在可观察动作集合中的外部动作才被视为可观察的外部动作。
对于一个 while 循环不断输出 print("hello") 的程序,提取任意多(但有限)的、从第一个开始的 print 操作(例如 {print("hello")_1, ..., print("hello")_n}),都符合第一条定义的“行为 B 是允许行为”。这是因为 B 是有限的可观察动作集合 O 中的外部动作集合,且程序没有阻塞或挂起行为。
7.1 Discussion
这跟都市传说一样的,1. 如果观察到了,必须停止,2. 如果挂起就可以观察不到。
如果重排序,就会“既打印了,又不停止”。
为什么可能不停止?我们就让线程1一直执行吧。
8. SURPRISING AND CONTROVERSIAL BEHAVIORS
一些有趣的例子。
总体来说,就是有漏风,如果只漏风一半,那就不会透出去,如果全漏,那就透出去了。
9 待补充
10 待补充