zgq105 / blog

2 stars 0 forks source link

Java并发-并发理论(JMM) #90

Open zgq105 opened 4 years ago

zgq105 commented 4 years ago

1. JMM内存模型

1.1 java内存模型和硬件架构模型

内部Java内存模型 JVM内部使用的是java内存模型在线程堆栈和堆之间分配内存。如下图所示: image

如果把线程堆栈的调用过程考虑进来,那么就得到如下图所示: image

jvm内存模型满足以下特性:

  1. 局部变量如果是原始类型(int、double 、byte等),则完全在线程栈中分配内存。
  2. 局部变量如果是引用类型(Integer、Double、Object等),局部变量对象的引用存储在线程栈中,而对象本身存储在堆中。
  3. 对象的成员变量和对象本身存储在堆中。
  4. 静态类变量也与类定义一起存储在堆中。 5.多个不同线程的局部变量可以对堆中同一个对象的引用。

硬件内存架构 现代硬件内存体系结构与内部Java内存模型有所不同。硬件架构内存模型是计算机真是存在物体体系架构;而java内存模型是JVM抽象模型。具体的硬件架构如下所示: image

现代计算机中一般会有多个CPU或者多核CPU,因此存在同时运行多个线程的情况。每个CPU有一组寄存器,CPU寄存器本质上是CPU内存。CPU在寄存器上的执行速度要远快于在主内存上的执行速度。

每个CPU可能还会存在高速缓存层。实际上,大多数现代CPU都具有一定大小的缓存层。CPU可以比其主存储器更快地访问其高速缓存,但是通常不如它可以访问其内部寄存器的速度快。比如,我的计算机是有三级缓存,如下所示: image

综上所述,CPU执行速度:寄存器>CPU缓存>主存储器。

结合Java内存模型和硬件内存体系结构 java内存模型和硬件内存体系是不同的。硬件内存架构是不区分栈和堆的。在硬件上,线程的栈和堆都位于主内存中,有时部分栈和堆还会出现在CPU缓存或者寄存器中。如下所示:

1.2 线程共享数据

线程共享数据主要包括以下数据:

  1. 存储在主内存中的共享变量。

1.3 线程私有数据

  1. 局部变量。
  2. 程序计数器。
  3. 共享变量的副本。

1.4 工作内存和主内存通信

image

JVM内存交互协议 线程和主内存之间的交互过程rux如下所示: image

java内存模型定义了一个8种操作来完成线程之间的交互,如下:

2. 重排序

为什么要指令重排序? 在java中,java编译的过程中和处理器执行指令过程中可能会出现指令重排序。指令重排的作用是提升程序的执行效率。因为CPU执行指令,本质上是流水线作业,可以通过调整执行的顺序来优化工作的路程,从而提升程序的执行效率。 代码片段1

int a = 1;
int b = 1;
a = a + 1;
b = b +1 ;

代码片段2

int a = 1;
a = a + 1;
int b = 1;
b = b +1 ;

从上面的两段代码中,我们知道代码片段2的执行效率会高于代码片段1;因为代码片段2把相似功能单元的指令接连执行来减少流水线中断的情况。

编译器重排序会按JMM的规范严格进行,换言之编译器重排序一般不会对程序的正确逻辑造成影响。但是处理器重排序就需要使用到内存屏障了。

指令重排遵循的规则 单线程情况下,当存在数据依赖时,指令不会发生重排。如下情形所示:

以上情况,指令不会重排;但是在多线程环境下失效。

什么情形需要禁止指令重排? 在多线程环境时,指令重排会产生不确定的执行效果。如下面代码所示:

public class ReorderExample {
    int a = 0;
    boolean flag = false;

    public void writer() {
        a = 1;                   //1
        flag = true;             //2
    }

    public void reader() {
        if (flag) {                //3
            int i =  a * a;        //4
            System.out.println(i);
        }
    }

}

说明:由于1和2是没有数据依赖的,故可能会发生指令重排的;如果执行的指令重排,则执行的结果就有多种结果了。因此,在多线程环境中,需要注意指令重排造成的逻辑不一致的问题。

如何防止指令重排 在java中,volatile关键字可以保证变量的可见性和防止指令重排。关于volatile详细用法,这里暂时不展开阐述。

什么是内存屏障? 内存屏障是jvm中的一种规则,是一种CPU指令;用于控制特定条件下的重排序和内存可见性问题。Java编译器也会根据内存屏障的规则禁止重排序。volatile关键字就是通过内存屏障实现了可见性和有序性。

3. happens-before原则

  1. 程序顺序规则:一个线程中的每个操作,happens-before于该线程中的任意后续操作。
  2. 监视器锁规则:对一个锁的解锁,happens-before于随后对这个锁的加锁。
  3. volatile变量规则:对一个volatile域的写,happens-before于任意后续对这个volatile域的读。
  4. 线程启动规则:如果线程A执行操作ThreadB.start()(启动线程B),那么A线程的ThreadB.start()操作happens-before于线程B中的任意操作。
  5. 线程终结规则:如果线程A执行操作ThreadB.join()并成功返回,那么线程B中的任意操作happens-6 before于线程A从ThreadB.join()操作成功返回。
  6. 传递性规则:如果A happens-before B,且B happens-before C,那么A happens-before C。