JVM内部机制(七) Java内存模型(Java Momory Model)

分类: JVM 发布于:

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允许的意外情况

jmm

本文解释一下在JDK8中,JVM如何执行方法。

TODO