Android---java内存模型与线程

Java 内存模型翻译自 Java Memory Model,简称 JMM。它所描述的是多线程并发、CPU 缓存等方面的内容。

Android---java内存模型与线程_第1张图片

在每一个线程中,都会有一块内部的工作内存,这块内存保存了主内存共享数据的拷贝副本。但在 Java 线程中并不存在所谓的工作内存(working memory),它只是对 CPU 寄存器和高速缓存的抽象描述

CPU 普及

Android---java内存模型与线程_第2张图片

线程是 CPU 调度的最小单位,线程中的字节码指令最终都在 CPU 中执行。CPU 在执行的时候,免不了要和各种数据打交道,而 Java 中所有数据都是放在主内存中的。随着 CPU 的发展,CPU 的执行速度越来越快,但内存的技术并没有太大变化,所以在内存中读取和写入数据的过程和 CPU 执行速度比起来,差距会越来越大。如上图箭头所示,CPU 对主内存的访问需要等待较长的时间,这样就体现不出 CPU 超强的运算能力了。

因此为了压榨处理器性能,达到高并发的效果,在 CPU 中添加了高速缓存 cache 来作为缓冲。在执行运算时,CPU 会将运算所使用到的数据复制到高速缓存中,让运算能够快速进行。当运算结束后,再将缓存中的结果刷回主内存。这样 CPU 就不用等待主内存的读写操作了。如下图所示:

Android---java内存模型与线程_第3张图片

 上面的方案,看起来一切正常。但问题也随之而来,每个 CPU 处理器都有自己的高速缓存,同时又共同操作同一块主内存,当多个处理器同时操作主内存时,可能导致数据不一致,这就是缓存一致性问题

 缓存一致性问题

现在,市面上的手机通常有2个或多个 CPU,其中一些 CPU 还有多核。每个 CPU 在某一时刻都能运行一个线程。如果 Java 程序是多线程的,就有可能存在多个线程在同一时刻被不同的 CPU 执行的情况

如下代码所示:

public int x = 0;
public int y = 0;

Thread p1 = new Thread(){
    public void run(){
        int r1 = x;
        y = 1;
    }
};

Thread p2 = new Thread(){
    public void run(){
        int r2 = y;
        x = 2;
    }
};

p1.start();
p2.start();

假设我们的一台设备上有2个CPU,分别为 C1 和 C2,将上面这段代码执行在这台设备上,最后打印出的 r1 和 r2 值分别是多少?答案是不确定的。

情况1:r1 = 0, r2 = 1

假设 P1 现在 C1 中执行完毕,并成功刷新回主内存中,此时 r1 = 0, x = 0, y =1。然后 P2 在 C2 中执行,从主内存中加载 y =1 ,并赋值给 r2。此时 r2 = 1, x = 2, y = 1, r1 = 0。

情况2:r1 = 2, r2 = 0

假设 P2 现在 C1 中执行完毕,并成功刷新回主内存中,此时 r2 = 0, x = 2, y =0。然后 P1 在 C2 中执行,从主内存中加载 x =2 ,并赋值给 r2。此时 r1= 2, x = 2, y = 1, r2 = 0。

情况3:r1 = 0, r2 = 0(特殊)

x, y的值分别缓存在 C1 和 C2 的缓存中,首先 P1 在 C1 中执行完毕,但是并未将结果刷新回主内存中,此时主内存中的 x = 0, y = 0。然后 P2 在 C2 中执行,缓存中的 y = 0, 将其赋值给 r2,此时,r2 = 0 , x = 2, y = 1,如下图所示:

Android---java内存模型与线程_第4张图片

虽然 C1 和 C2 的主内存中修改了 x, y 的值,但并未将它们刷新到主内存中。这就是缓存一致性问题

指令重排

除了缓存一致性问题,还存在另外一种硬件问题,即指令重排。为了使 CPU 内部的运算单元能够尽量充分利用,处理器可能会对输入的字节码指令进行重排序处理,也就是处理器优化。比如 Java 虚拟机的即时编译器(JIT),如以下代码,

a = 1;
b = 2; 
a = a + 1

编译后的字节码指令如下:

Android---java内存模型与线程_第5张图片

可以看出,在上述红框中的指令是表达的同一种语义,并且指令7并不依赖指令2和3,在这种情况下,CPU 会对指令的顺序做如下优化。

Android---java内存模型与线程_第6张图片

从 Java 语言的角度看这种优化如下:

Android---java内存模型与线程_第7张图片

也就是说在 CPU 层面,Java 有时候并不会严格按照文件中的顺序去执行。

缓存一致性问题和指令重排的内容表明,如果我们任由 CPU 优化,那么我们编写的 java 代码所执行的效果会大大出乎我们的意料。

为了解决这个问题,让 Java 代码在不同硬件、不同操作系统中输出的结果达到一致。Java 虚拟机规范提出了一套机制--> java 内存模型

Java 内存模型

内存模型是一套共享内存系统中多线程读写操作行为的规范。这套规范屏蔽了底层各种硬件和操作系统的内存访问差异。解决了 CPU 多级缓存、CPU优化、指令重排等导致的内存访问问题,从而保证 Java 程序(尤其是多线程程序)在各种平台下对内存的访问效果一致。

在 java 内存模型中,统一用工作内存来当作 CPU 中寄存器或高速缓存的抽象。线程之间的共享变量存储在主内存中,每个线程都有一个私有工作内存(类比 CPU 中的寄存器或者高速缓存),本地工作内存中存储了该线程读/写共享变量的副本。

在这套规范中有一个非常重要的规则 happens-before 先行发生原则。

happens-before 先行发生原则

happens-before 用于描述两个操作的内存可见性,通过保证可见性的机制可以让应用程序免于数据竞争干扰。

定义如果一个操作 A happens-before 另一个操作 B,那么操作 A 的执行结果将对操作 B 可见

反过来理解,如果操作 A 的结果必须对操作 B 可见,那么操作 A 必须 happens-before 操作 B。

举例:

private int value = 0;
 
public void setValue(int value){
    value = 1;
}

public int getValue(){
    return value;
}

假设 setValue 就是操作 A,getValue 就是操作 B,如果先后在两个线程中调用 A 和 B,那最后在 B 操作中返回的 value 值是多少呢?

情况1:A happens-before B 不成立

当线程调用操作 B 时,即使操作 A 已经在其它线程中被调用过,并且 value 也被成功设置为1。但这个修改对操作 B 仍然是不可见的,根据上面 CPU 缓存的内容, value 值有可能返回1,也有可能返回0。

情况2:A happens-before B 成立

根据 happens-before 的定义,先行发生动作的结果对后续动作是可见的,也就是先执行 A  后的结果对后续的操作 B 是始终可见的。即,先调用 setValue() 将 value 的值修改为1,后续在其它线程中调用 getValue() 获得的 value 值一定是1。

在 Java 中的两个操作如何算符合 happens-before 规则了呢?

JMM 中定义了以下几种情况是自动符合 happens-before 规则的。

1. 程序次序规则

在单线程内部,如果一段代码的字节码顺序也隐私符合 happens-before 原则,那么逻辑顺序靠前的字节码执行结果一定是对后续逻辑字节码可见。如下代码所示:
 

int a = 10; // 1
b = b + 1; //2

当代码执行到2处时,a = 10 这个结果已经是公之于众的,至于用没用到 a  这个结果则不一定,比如上面的代码中没有用到 a = 10 这个结果。

2. 锁定规则

一个锁如果处于被锁定状态,那么必须先执行 unlock 操作后才能进行 lock 操作。

3. 变量规则

volatile 保证了线程可见性。如果一个线程先写了一个 volatile 变量,然后另一个线程去读这个变量,那么这个写操作一定是 happens-before 读操作的。

4. 线程启动规则

Thread 对象的 start() 方法先行发生于此线程的每一个动作。假定线程 A 在执行过程中,通过执行 ThreadB.start() 来启动线程 B,那么线程 A 对共享变量的修改在线程 B 开始执行后确保对线程 B 可见。

4. 线程中断规则

对线程interrupt()方法的调用先行发生于被中断线程的代码检测,直到中断事件的发生。

5. 线程终结规则

线程中所有的操作都发生在线程的终止检测之前,可以通过Thread.join()方法结束、Thread.isAlive() 的返回值等方法检测线程是否终止执行。假定线程A在执行的过程中,通过调用ThreadB.join()等待线程B终止,那么线程B在终止之前对共享变量的修改在线程A等待返回后可见。

6. 对象终结规则

一个对象的初始化完成发生在它的finalize()方法开始前。

happens-before 原则具有传递性

如果操作 A happens-before 操作 B,而操作 B happens-before 操作 C,那么操作 A 一定 happens-before 操作 C。

Java 内存模型应用

根据 happens-before 原则,能够解决在并发环境下操作之间是否可能存在冲突的所有问题。可以可以通过 Java 提供的一系列关键字,将实现的多线程操作“happens-before 化”。happens-before 化”是将本来不符合 happens-before 原则的某些操作,通过某种手段使它们符合 happens-before 原则。

方法1:使用 volatile 关键字

private volatile int value = 0;

public void setValue(int value){
    value = 1;
}

public int getValue(){
    return value;
}

方法2:使用 synchronized 关键字修饰操作

private int value = 0;

public void setValue(int value){
    synchronized{
        value = 1;
    }
}

public int getValue(){
    synchronized{
        return value;
    }
}

通过以上两个方式都可以使 setVaule() 和 getValue() 符合 happens-before 原则。当在某一线程中调用 setVaule()后,再在其它线程中调用 getValue() 获取到的值一定是 1。

总结

Java 内存模型的来源:

主要是因为 CPU 缓存指令重排等优化操作造成多线程程序结果不可控。

Java 内存模型是什么:

本质上它就是一套规范,在这套规范中有一条最重要的 happens-before 原则。

Java 内存模型的使用

简单介绍了两种方式:volatile 和 synchronized。
 

你可能感兴趣的:(#,Android进阶,java,开发语言)