【Java】Java内存模型

1、现代计算机内存模型

早期的计算机中因为CPU和内存的速度是差很少的,因此CPU是直接访问内存地址的。而在现代计算机中,CPU指令的运行速度远远超过了内存数据的读写速度,为了下降这二者间这高达几个数量级的差距,因此在CPU与主内存之间加入了CPU高速缓存。java

高速缓存能够很好地解决CPU与主内存之间的速度差距,但CPU缓存并非全部CPU共享的,所以产生了一个新的问题:数据一致性问题程序员

2、缓存一致性协议(MESI)

CPU缓存的一致性问题会致使并发处理的不一样步,对于这个问题,大概有如下两种方案:编程

  1. 总线加锁 ---> 下降了CPU的吞吐量,不现实
  2. 采用缓存上的一致性协议MESI ---> 现代处理器经常使用,或使用其变种的协议

1. MESI四种状态

MESI 这个名称自己是由Modified(修改)、Exclusive(独享)、Shared(共享)、Invalid(无效)。这个四个单词也表明了缓存协议中对缓存行(即Cache Line,缓存存储数据的单元)声明的四种状态,用2 bit表示,它们所表明的含义以下所示:缓存

状态 描述 监放任务
M修改(Modified) 这行数据有效,数据被修改了,和内存种的数据不一致,数据只存在于本Cache中 缓存行必须时刻监听全部试图读该缓存行相对就主存的操做,这种操做必须在缓存将该缓存行写回主存并将状态变成S(共享)状态以前被延迟执行。
E独享(Exclusive) 这行数据有效,数据和内存中的数据一致,数据只存在于本Cache中 缓存行也必须监听其它缓存读主存中该缓存行的操做,一旦有这种操做,该缓存行须要变成S(共享)状态。
S共享(Shared) 这行数据有效,数据和内存中的数据一致,数据存在于不少Cache中 缓存行也必须监听其它缓存使该缓存行无效或者独享该缓存行的请求,并将该缓存行变成无效(Invalid)。
I无效(Invalid) 这行数据无效
  • E状态示例以下:多线程

    只有Core 0访问变量x,它的Cache Line状态为E。并发

  • S状态示例以下:app

    3个Core都访问变量x,它们对应的Cache line为S(Shared)状态。性能

  • M状态和I状态示例以下:优化

    Core 0 修改了x的值以后,这个Cache Line变成了M(Modified)状态,其余Core对应的Cache line变成了I(Invalid)状态。.net

2. 状态间的迁移

在MESI协议中,每一个Cache的cache控制器不只知道本身的读写操做,并且也监听其余cache的读写操做。每一个Cache Line所处的状态根据本核和其余核的操做在4个状态间进行迁移。

在上图中,Local Read表示本内核读本Cache中的值,Local Write表示本内核写本Cache中的值,Remote Read表示其它内核读其它Cache中的值,Remote Write表示其它内核写其它Cache中的值,箭头表示本Cache line状态的迁移,环形箭头表示状态不变。

当内核须要访问的数据不在本Cache中,而其它Cache有这份数据的备份时,本Cache既能够从内存中导入数据,也能够从其它Cache中导入数据,不一样的处理器会有不一样的选择。

本文只进行简单介绍,具体请阅读Cache一致性协议之MESI

3. 如何保证缓存一致性

了解完什么是MESI,那么具体是如何保证缓存一致性的呢?

《Java并发编程的艺术》中提到:在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议,每一个处理器经过嗅探在总线上传播的数据来检查本身缓存中的值是否是过时了,当处理器发现本身缓存行对应的内存地址被修改,就会将当前处理器的缓存行设置为无效状态,当处理器对这个数据进行修改操做的时候,会从新从系统内存中把数据读处处理器缓存里。

3、Java内存模型

1. Java内存模型的抽象结构

线程之间的共享变量存储在主内存(Main Memory)中,每一个线程都有一个私有的本地工做内存(Local Memory),工做内存中存储了线程以读/写共享变量的副本。(本地工做内存是 JMM 的一个抽象概念,并不真实存在,线程中所谓工做内存其实仍是存在于主内存中的。)

2. Java内存模型与现代计算机内存模型区分

Java内存模型和现代计算机内存模型都须要解决一致性问题,可是这个一致性问题在现代计算机内存模型中指代的是缓存一致性问题,MESI协议所设计的目的也是为了解决这个问题。而在Java内存模型中,这个一致性问题则是指代内存一致性问题。二者之间有必定区别。

  • 缓存一致性

    计算机数据须要通过内存、计算机缓存再到寄存器,计算机缓存一致性是指硬件层面的问题,指的是因为多核计算机中有多套缓存,各个缓存之间的数据不一致问题。缓存一致性协议(如MESI)就是用来解决多个缓存副本之间的数据一致性问题。

  • 内存一致性

    线程的数据则是放在内存中,共享副本也是,内存一致性保证的是多线程程序并发时的数据一致性问题。咱们常见的volatile、synchronized关键字就是用来解决内存一致性问题。这里屏蔽了计算机硬件问题,主要解决原子性、可见性和有序性问题。

至于内存一致性与缓存一致性问题之间的关系,就是实现内存一致性时须要利用到底层的缓存一致性(以后的volatile关键字会涉及)。

4、并发编程的特性

首先咱们要先了解并发编程的三大特性:原子性,可见性,有序性;

1. 原子性

原子性是指一个操做是不可间断的,即便是多个线程同时执行,该操做也不会被其余线程所干扰。

咱们来看一下Java中几条常见的指令是否具备原子性

  • x = 10

    private int x;
    
    // 具备原子性
    x = 10;
  • i++

    不具有原子性,由于i++包括了如下三个步骤:

    1. 读取 i 的值到内存空间
    2. i + 1
    3. 刷新结果到内存
  • y =x

    private int x, y;
    x = 10;
    
    /*
    y = x没有原子性
    	1. 把数据x读到工做空间(这一步具备原子性)
    	2. 把x的值写到y中(这一步也具备原子性)
    */
    y = x;

总结:多个原子性的操做结合在一块儿的操做并不具有原子性。

2. 可见性

内存可见性(Memory visibility)是指当某个线程正在使用对象状态而同时另外一个线程正在修改该状态,此时须要确保当一个线程修改了对象状态后,其余线程可以看到发生的状态变化。

正如咱们上面所说的,每一个线程都有一个私有的本地工做内存并存储了线程间读/写的共享副本。因此当一个线程对这个副本进行修改而没有将这个修改后的值写入主内存中,亦或者这个修改后的值写入了主内存中而其余线程并无去访问主内存中的值,依旧使用的是本地工做内存中的值,那么此时的并发就有产生问题。咱们来看下面代码:

public class NoVisibility {

    public static boolean ready = false;

    private static class ReaderThread extends Thread {
        public void run() {
            while (true) {
                if (ready) {
                    System.out.println("=== 即将结束循环 ===");
                    break;
                }
            }
        }
    }

    public static void main(String[] args) throws InterruptedException {
        new ReaderThread().start();
        Thread.sleep(2000);
        ready = true;
        System.out.println("ready = " + ready);
    }

}

上面代码极可能会持续循环下去,永远不会打印出"=== 循环即将结束 ==="这段文字。咱们在代码中能够显而易见的有两个线程,主线程和咱们声明的ReaderThread线程。 ready 这个变量在ReaderThread线程开始循环时就已经被复制一份到本地工做内存中了,当主线程修改ready的值为true时,此修改对于其余线程并不可见,ReaderThread线程并无去读取新的值,一直使用本地工做内存中的值,因此会形成无限循环。

对于这个问题很好解决,只要将ready变量声明为volatile变量便可。

3. 有序性

有序性即程序按照咱们代码所书写的那样,按其前后顺序执行。第一次接触这个特性可能会有所疑惑,因此在了解有序性以前咱们须要来了解执行重排序以及相关概念。

3.1 指令重排序

为了提升性能,编译器和处理器会对程序的指令作重排序操做,重排序分为3种类型:

  1. 编译器优化的重排序:属于编译器重排序,编译器在不改变单线程程序语义的前提下,能够从新安排语句的执行顺序;
  2. 指令级并行的重排序:属于处理器重排序,现代处理器采用指令级并行技术来将多条指令重叠执行。若是不存在数据依赖,处理器能够改变语句对应机器指令的执行顺序;
  3. 内存系统的重排序:处于处理器重排序因为处理器使用缓存和读/写缓冲区,这使得加载和存储操做看上去多是在乱序执行。

指令重排序对于程序执行有利有弊,咱们并非要去彻底禁止它。对于编译器重排序,JMM的编译器重排序规则会禁止特定类型的编译器重排序。对于处理器重排序,JMM的处理器重排序规则会要求Java编译器在生成指令序列时,插入特定类型个的内存屏障指令,经过内存屏障指令来禁止特定的处理器重排序。

3.2 as-if-serial

as-if-serial语义的意思是:无论怎么重排序,(单线程)程序的执行结果不能被改变。编译器、runtime和处理器都必须遵照as-if-serial语义。

为了遵照as-if-serial语义,编译器和处理器不会对存在数据依赖关系的操做作重排序。由于这种重排序会改变执行结果。可是,若是操做之间不存在数据依赖关系,这些操做就可能被编译器和处理器重排序。为了具体说明,请看下面计算圆面积的代码示例:

double pi = 3.14;         // A
double r = 1.0;           // B
double area = pi * r * r  // C

如上个图所示,A和C之间存在数据依赖关系,同时B和C之间也存在数据依赖关系。一次在最终执行的指令序列种,C不能被重排序到A和B前面(C排到A和B的前面,程序的结果将会被改变)。但A和B之间没有数据依赖关系,编译器和处理器能够重排序A和B之间的执行顺序。下图是该程序的两种执行顺序:

as-if-serial语义把单线程程序保护起来,遵照as-if-serial语义的编译器、runtime和处理器共同为编写单线程程序的程序员建立一个幻觉:单线程程序是按程序顺序来执行的。as-if-serial语义使单线程程序员无需担忧重排序会干扰他们,也无需担忧内存可见性问题。

3.3 happens-before

若是说 as-if-serial 是 JMM 提供用来解决单线程间的内存可见性问题的话,那么 happens-before 就是JMM向程序员提供的可跨越线程的内存可见性保证。具体表现为:若是线程A的写操做a与线程B的读操做b之间具备 happens-before 关系,那么JMM将保证这个操做a对操做b可见。此外,happens-before 还有传递关系,表现为:a happens-before b,b happens-before c,那么a happens-before c。

注意:两个操做之间存在happens-before关系,并不意味着一个操做必需要在后一个操做以前执行,只要求前一个操做执行的结果对后一个操做可见。若是重排序以后的执行结果,与按happens-before关系来执行的结果一致,那么这种重排序并不违法(也就是说,JMM容许这种重排序)。

比对 happens-before 与 as-if-serial。

  1. as-if-serial语义保证单线程内程序的执行结果不被改变,happens-before关系保证正确同步的多线程程序的执行结果不被改变。

  2. as-if-serial语义给编写单线程程序的程序员创造了一个幻境:单线程程序是按程序的顺序来执行的。happens-before关系给编写正确同步的多线程程序的程序员创造了一个幻境:正确同步的多线程程序是按 happens-before 指定的顺序来执行的。

  3. as-if-serial 语义和 happens-before 这么作的目的,都是为了在不改变程序执行结果的前提下,尽量地提升程序执行的并行度。

因此,总的说来 happens-before 与 as-if-serial 在本质上是同一种概念。

5、volatile变量

volatile能够视为轻量级的synchronized,能够确保共享变量在各个线程间的“可见性”。

1. volatile内存语义

咱们能够将volatile变量的读写操做分别视之为 get 方法和 set 方法,因此从内存可见性的角度看,写入volatile变量至关于退出同步代码块,而读取volatile变量就至关于进入同步代码块。

2. volatile缓存可见性实现原理

底层实现主要是经过汇编lock前缀指令,它会锁定这块内存区域的缓存(缓存行锁定)并回写到主内存。其中lock前缀指令在多核处理器下会引起两件事情:

  1. 会将当前处理器缓存行的数据当即回写到系统内存。
  2. 这个写回内存的操做会引发在其余CPU里缓存了该内存地址的数据无效(经过MESI缓存一致性协议)。

3. volatile的应用

volatile变量的一个种典型的用法:检查某个状态标记以判断是否退出循环。

还有单例模式的实现,典型的双重检查锁定(即DCL)。

参考

相关文章
相关标签/搜索