JMM Java 内存模型

Java 内存模型

Java Memory Model , 为 java 内存模型, 简称为 JMM .

参考链接

深入理解java内存模型系列文章

主要参考为上述链接,上述讲的特别好,很清楚,很详细。

JMM 解决可见性的问题(同步包括独占性和可见性)

Java 内存模型的主要目的是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存和从内存中取出变量。

两个名词含义:

主内存: 线程之间共享变量存储在主内存中

工作内存: 每个线程都有一个私有的本地内存, 称为工作内存,里面存放着该线程以读/写共享变量的副本.

JMM 同时定义了线程和主内存之间的抽象关系.

  1. JMM 通过控制主内存与每个线程的本地内存之间的交互, 提供了内存可见性保证。

  2. Java 线程间的通信由 JMM 控制

    JMM 决定一个线程对共享变量的写入何时对另一个线程可见

  3. Java 内存模型规定了所有的变量都存储在主内存中,每条线程还有自己的工作内存(与cache类比)

    线程对变量的所有操作(读取、赋值)都必须在工作内存中进行,而不能直接读写内存中的变量。

    Java 线程 <-----> 工作内存 <------> savaload 操作 <-----> 主内存

主内存和工作内存之间的具体交互协议

Java 内存模型定义了一下八种操作:

  1. lock : ----> 主内存变量, 把一个变量标识为一条线程独占状态
  2. Unlock : ----> 主内存变量, 释放
  3. read : ----> 主内存变量, 把一个变量值从内存传送到线程的工作内存 ,便于load 操作
  4. load : ----> 工作内存的变量,把read的变量值放入到工作内存的变量副本中
  5. use : ----> 工作内存的变量,工作内存的变量值--> 执行引擎
  6. assign : ----> 工作内存的变量, 把从执行引擎得到的值----> 赋给工作内存的变量
  7. store : ----> 工作内存的变量, 传给主内存,便于后面的write操作
  8. write : ----> 主内存的变量,把从工作内存的传过来的变量进行write进主内存

Java内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行

happens-before 原则

JMM 中,如果一个操作执行的结果需要对另外一个操作可见,那么这两个操作之间必需要存在 happens-before 关系.

上述两个操作,可在同一个线程里,也可在不同的线程中.

规则如下:

  1. 程序顺序规则:

    一个线程中的每个操作,happens- before 作用于该线程中的任意后续操作.

  2. 监视器规则

    对一个监视器锁的解锁,happens- before 作用于随后对这个监视器锁的加锁.

  3. volatile 变量原则

    对一个 volatile 域的写,happens- before 作用于任意后续对这个 volatile 域的读

  4. 传递性

    如果 A happens- before B,且 B happens- before C,那么 A happens- before C.

happens- before 的理解

两个操作之间存在 happens- before, 并不是意味着 前一个操作必需在后一个操作执行之前执行, 而是仅仅要求前一个操作的结果(执行后的结果),对后一个操作可见, 且前一个操作按顺序排在第二个操作之前(代码间存在重排序问题).

重排序

源代码----> 编译器优化重排序 ----> 指令级并行重排序 -----> 内存系统重排序 ----> 最终执行的指令序列

为了保证内存的可见性,Java编译器在生成指令序列的适当位置会插入内存屏障指令来禁止特定类型的处理器重排序 , 因为重排序可能会导致多线程程序出现内存可见性问题。

JMM 特点

  1. Java内存模型JMM 确保在不同的编辑器和不同的处理器平台上,通过禁止特定的的编译器重排序和处理器重排序,为程序员提供一致的内存可见性。

  2. Java内存模型JMM 不会破坏已有的 数据依赖性as-if-serial 语义程序顺序规则. (具体含义见下面)

Java内存模型把内存屏障分为LoadLoad、LoadStore、StoreLoad和StoreStore四种:

image

数据依赖性

编译器和处理器在重排序时,会遵守数据依赖性。

  • 数据依赖性仅针对单个处理器中执行的指令序列和单个线程中执行的操作,不同处理器之间和不同线程之间的数据依赖性不被编译器和处理器考虑

as-if-serial 语义

不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。
编译器,runtime 和处理器都必须遵守as-if-serial语义.

happens- before 程序顺序规则

JMM仅仅要求前一个操作(执行的结果)对后一个操作可见,且前一个操作按顺序排在第二个操作之前

JMM允许不影响操作结果的重排序

重排序对多线程的影响

  • 多线程程序中,对存在控制依赖的操作重排序,可能会改变程序的执行结果。

  • 单线程程序中, 对存在控制依赖的操作重排序,不会改变执行结果。

数据竞争与顺序一致性保证

当程序未正确同步时,就会存在数据竞争

在Java内存模型对数据竞争的定义如下:

  1. 在一个线程中写一个变量
  2. 在另一个线程中读同一个变量
  3. 写和读没有通过同步来排序

如果一个多线程程序能正确同步,这个程序将是一个没有数据竞争的程序,正确同步的多线程程序

JMM 对正确同步的多线程程序做了如下保证:

  • 如果程序是正确同步的,则程序的执行将具有顺序一致性, 即程序的执行结果与该程序在顺序一致性内存模型中的执行结果相同,

顺序一致性内存模型

顺序一致性内存模型 是一个被计算机科学家理想化了的理论参考模型,提供了极强的内存可见性保证

与Java 内存模型不同
特性:

  • 一个线程中的所有操作必须按照程序的顺序来执行
  • (不管程序是否同步)所有线程都只能看到一个单一的操作执行顺序
  • 在顺序一致性内存模型中,每个操作都必须原子执行且立刻对所有线程可见。

同步机制 :

volatile , synchronized, final

  1. volatile 关键字

    volatile修饰的变量,线程在每次使用变量的时候,都会读取修改后的最新的值.

    对变量的写操作,不依赖于当前值

    不能和其他变量同时出现在一个表达式中

    volatile的作用就是使它修饰的变量的读写操作都必须在内存中进行

    • 可见性:对一个 volatile 变量的读,总是能看到(任意线程)对这个 volatile 变量最后的写入.
    • 原子性:对任意 volatile变量的读/写都具有原子性

    volatile 变量的写-读可以实现线程之间的通信, 如下:

    • 线程A写一个volatile变量, 实质上是线程A向接下来将要读这个volatile变量的某个线程发出了(其对共享变量所在修改的)消息
    • 线程B读一个volatile变量,实质上是线程B接收了之前某个线程发出的(在写这个volatile变量之前对共享变量所做修改的)消息
    • 线程A写一个volatile变量,随后线程B读这个volatile变量,这个过程实质上是线程A通过主内存向线程B发送消息

    volatile 变量的写-读 的内存意义

    • 当写一个 volatile 变量时, JMM 会把该线程对应的本地内存中的共享变量刷新到主内存;

    • 当读一个 volatile 变量时, JMM 会把该线程对应的本地内存置为无效,线程接下来将从主内存中读取共享变量.

    为了达到上述的内存意义效果,编译器在生成字节码时,会在指令序列中插入内存屏障来禁止特定类型的处理器重排序, 如下,采用保守策略的 JMM 内存屏障插入策略:

    • 在每个 volatile 写操作的前面插入一个 StoreStore 屏障
    • 在每个 volatile 写操作的后面插入一个 StoreLoad 屏障
    • 在每个 volatile 读操作的后面插入一个 LoadLoad 屏障
    • 在每个 volatile 读操作的后面插入一个 LoadStore 屏障
  1. synchronized 关键字

    线程同步的内部机制,通过加锁的方式保证线程的可见性和互斥性,
    即一个线程的执行结果可以被另一个线程所看到且在同一时间只能有一个线程执行被保护的代码块.

    锁释放和获取的内存语义:

    • 当线程释放锁时,JMM 会把该线程对应的本地内存中的共享变量刷新到主内存中

    • 当线程获取锁时,JMM会把该线程对应的本地内存置为无效,从而使得被监视器保护的临界区代码必须要从主内存中去读取共享变量

    如下消息传递:

    • 线程A释放一个锁,实质上是线程A向接下来将要获取这个锁的某个线程发出了(线程A对共享变量所做修改的)消息;
    • 线程B获取一个锁,实质上是线程B接收了之前某个线程发出的(在释放这个锁之前对共享变量所做修改的)消息;
    • 线程A释放锁,随后线程B获取这个锁,这个过程实质上是线程A通过主内存向线程B发送消息
  2. final 关键字

    与前面的锁和volatile相比较,对final域的读和写更像是普通的变量访问

    • 在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
    • 初次读一个包含final域的对象的引用,与随后初次读这个final域,这两个操作之间不能重排序

上面的两条规则其实可分为

1.写final域的重排序规则

  1. 读final域的重排序规则

final 域的重排序规则

  • JMM禁止编译器把final域的写重排序到构造函数之外

  • 编译器会在 final 域的写之后,构造函数 return 之前,插入一个 StoreStore 屏障(内存屏障)
    这个屏障禁止处理器把 final 域的写重排序到构造函数之外。

final 域的重排序规则

  • 在一个线程中,初次读对象引用与初次读该对象包含的 final 域,JMM 禁止处理器重排序这两个操作
    仅针对与处理器(重排序的后两种),
    编译器会在读final域操作的前面插入一个 LoadLoad 屏障

volatile与synchronized 的同异

  1. volatile 本质是在告诉 jvm 当前变量在寄存器中的值是不确定的,需要从主存中读取。
  2. synchronized 则是锁定当前变量,只有当前线程可以访问该变量,其他线程被阻塞住.
  3. volatile 仅能使用在变量级别, synchronized 则可以使用在变量,方法.
  4. volatile 仅能实现变量的修改可见性,但不具备原子特性,而 synchronized 则可以保证变量的修改可见性和原子性
  5. volatile 不会造成线程的阻塞,而 synchronized 可能会造成线程的阻塞.
  6. volatile 标记的变量不会被编译器优化,而 synchronized 标记的变量可以被编译器优化.

如有错误,请指出,谢谢.

你可能感兴趣的:(JMM Java 内存模型)