JVM内部机制(七) Java内存模型(Java Momory Model)
Jvm内存模型解析
什么是内存模型
Java的内存模型和jvm中的内存layout是两个不同的概念。
Jvm中的Memory Layout,栈、栈帧、Heap[新生代(New Generation)、旧生代、MetaData区域]是一种内存结构,是为实现Java语言而设计的一种存储结构,它与jvm的运行时状态无关,只要运行Java程序,这种存贮结构就会被用到。
而JSR133中定义的Java内存模型是Java语言层面的,讨论的核心是指令集(Instruction Set)如何在N个线程(并行执行序列)的背景下,使得每个单一线程以正确、符合预期的方式表达。
那么,问题来了。
在什么情况下,指令集会以符合预期的方式运行?相反,什么条件使得结果让人难以接受,为什么?
Java的内存模型讨论的正是这种问题边界存在原因,以及最佳实践。
数据同步的两大问题类型
判断程序是否被正确同步,有两种方法:
Data Race
对共享的字段或者数组元素同时进行R-W(读写)操作,这种情况被称为“数据竞争”(data race)
⇒ Happen Before Relationship
happened-before 关系定义为:
如果一个事件(event) 发生在另外一个事件前, 那么, 它们的结果也是如此。
数学描述为
传递性(transitivity): ∀ a,b,c,如果 a → b, b → c, 那么 a → c
对任意的a,b,c, 如果a发生在b之前,b发生在c之前, 那么, a 发生在c之前
不自反特性: ∀a, a ↛ a
动作a不能在自身执行前发生
不对称特性: ∀ a, b , 如果 a ≠ b, 那么 a → b , b ↛ a
Java程序中,固定的Happens-Before动作(actions、method、or sinle instruction)发生的情况有:
- 同一个线程内,排在前面的指令(actions: read OR write)一定先执行。
或者说,同一线程内的动作是顺序执行(SO)的。
- 对监视器(monitor)的解锁动作(un-lock)一定发生在下一个锁动作(lock)之前。
这是锁资源的独占特性
-
对volatile字段的写(W)动作发生在对这个字段的读(R)动作之前
-
线程启动(Thread.start())发生已启动线程(Treads)的动作(actions)之前
-
某一活动线程的内动作(action)发生其他线程从join()成功返回之前
-
动作传递特性
对任意的a,b,c, 如果a发生在b之前,b发生在c之前, 那么, a 发生在c之前
产生数据同步问题的诱因
- 变量可见性(Visibility) → 不加控制的共享变量
class LoopMayNeverEnd {
boolean done = false;
// 线程 1
void work() {
while (!done) {
// do work
}
}
// 线程 2
void stopWork() {
done = true;
}
}
如果
线程1(r) $\xrightarrow[\text{Before}]{\text{happens}}$ 线程2(r)
那么,上面循环就编程了 while(true)了。
导致问题的原因是
线程1使用了其他线程也可以看到的变量。使用前并没有任务预防措施。
- Ordering → 编译器特权带来的同步问题
示例代码
class BadlyOrdered {
boolean a = false;
boolean b = false;
// 线程 1
void threadOne() {
a = true;
b = true;
}
// 线程 2
boolean threadTwo() {
boolean r1 = b; // sees true
boolean r2 = a; // sees false
return r1 && !r2; // returns true
}
}
上述两个类方法缺少同步,这导致了线程2读取a,b的时候, 线程1执行到了一半: a == true, b == false。
正确的方式应该是确保
threadOne() $\xrightarrow[\text{Before}]{\text{happens}}$threadTwo()
- 原子性(Atomicity) → 读写的原子性封装
同步问题的根源: 数据一致性
区分Actions和Executions
一个action被描述为一个tuple
$⟨t,k,u,v⟩$, 有4种类型的变量定义
-
t → 代表执行动作的线程
-
k → 代表同步动作(Synchronization Actions)的类型
volatile read
volatile write,
(normal or non-volatile) read
(normal or non-volatile) write
lock or unlock Volatile reads
volatile writes
locks and unlocks
-
v → 代表被actions用到的monitor或者变量
-
u → 此次动作的唯一ID
一个执行(Executions)也被定义为一个tuple:
⟨P,A,$\xrightarrow{po}$,$\xrightarrow{so}$,W,V,$\xrightarrow{\text{sw}}$,$\xrightarrow{\text{hb}}$⟩
-
P → 代表程序或者指令
-
A → 一组actions
-
$\xrightarrow{po}$ → A中被每个线程执行的actions的程序序列,这里不是指令序列。
po → 程序顺序指的是编码是代码的定义顺序,这里是所有actions中的po
-
$\xrightarrow{so}$ → A中的同步顺序,这里还是程序序列
-
W → 在E中(执行序列中)定义的,可以被读感知到的写动作,记做W(r)
-
V → 代表写动作执行后的值。对A中的写动作w,记做V(w),在执行序列E中写入的值。
-
$\xrightarrow{sw}$ → 代表同步关系
x $\xrightarrow{synchronizes with}$ y,意味着x与y同步
x = r1 (volatile write ⤞ 读取x的值之前,需要x的rlease状态)
y = x (volatile read ⤞ 读取x的值之前要等待x release结束)
- $\xrightarrow{hb}$ → Happens Before 关系
同步的定义
如果满足一下条件, 执行序列E被认为是同步的
1、每次读(read)到的值由执行序列E中的一次W决定。被定义为
∀ r ∈ A, 使得
可理解为: 写动作发生在当前E中,读到的值等于写后的值
2、代码执行顺序po和指令执行顺序同步顺序一致
不可用存在这种情况:
x,y ∈ A, x $\xrightarrow{so}$ y & y $\xrightarrow{po}$ x
可理解为,对任意x,y 属于A, 不能存在以下情况:
y同步于x,y的值有x决定。y却又先于x执行
3、多线程执行服从于单线程指令执行一致性(The execution obeys intra-thread consistency)
可以理解为
多线程的执行结果与把多线程改造为单线程执行后的结果一致
4、多线程服从Happen-before一致性规则
被Jmm允许的意外情况
本文解释一下在JDK8中,JVM如何执行方法。