全面理解Java内存模型(JMM)及volatile关键字01

文章目录

  • 理解Java内存区域与Java内存模型
    • Java内存区域
    • Java内存模型概述
  • 硬件内存架构与java内存模型
    • 硬件内存架构
    • java内存模型与硬件内存架构的关系
  • Java内存模型的特性
    • JMM内存模型出现的问题
      • 原子性
    • 理解指令重排
      • 编译器重排例子
      • 处理器重排例子
      • 有序性
      • 可见性
  • JMM提供的解决方案
    • 理解JMM中的happens-before原则
      • 具体七项规则
  • 分析volatile内存语义 通过内存屏障实现
    • volatile关键字的两个作用
    • volatile的可见性
    • volatile禁止指令重排序
  • 总结

理解Java内存区域与Java内存模型

Java内存区域

全面理解Java内存模型(JMM)及volatile关键字01_第1张图片
JVM运行时将管理的内存划分为以上区域。

  • 方法区:
    a) 方法区是线程共享的内存区域,主要存储JVM加载的**{类信息+常量+静态变量+即时编译器编译后数据}**
    b)当方法区无法满足内存分配需求时,会抛出OutOfMemoryError异常
    c)方法区存在一个运行时常量池存储编译器生成的{字面量+符号引用}
  • JVM栈:
    a)JVM栈是线程私有的内存区域,代表方法执行的内存模型。
    b) 每个方法执行时都会创建一个栈帧存储方法的变量表,操作数栈,返回值,返回地址
    JVM堆(GC堆):
    a)JVM堆是线程共享的内存区域,主要存储{对象实例}
    b) JVM堆是垃圾收集器管理的主要区域
    c) 当JVM堆无法满足内存分配需求时,会抛出OutOfMemoryError异常
  • 程序计数器
    a)程序计数器是线程私有内存区域,主要存储:当前线程 执行字节码的行号
    b)控制执行的字节码指令,分支,循环,跳转,异常处理
  • 本地方法栈:
    a) 本地方法栈是线程私有内存区域,主要存储JVM用到的native方法相关

Java内存模型概述

JMM是一种基于共享内存的并发模型,
它是一种抽象概念,规定了一种规范,规定了程序中变量的访问方式,
它规定所有变量都存储在主内存进而进行线程通信,而线程操作变量必须在工作内存进行。

线程,主内存,工作内存三者进行交互
全面理解Java内存模型(JMM)及volatile关键字01_第2张图片
主内存:
主要存储Java实例对象,不管该实例对象是成员变量还是方法的本地变量
或者是类信息,常量,静态变量
工作内存:
存储当前方法的本地变量,是主内存中变量的副本拷贝。
就算两个线程执行同一段代码,都会各自创建自己的工作内存,无法相互访问工作内存

全面理解Java内存模型(JMM)及volatile关键字01_第3张图片

硬件内存架构与java内存模型

硬件内存架构

目前计算机一般拥有多个cpu,每个cpu可能存在多个核心。这样就支持多任务并行。
从多线程调度角度来说,每个线程都会映射到各个 cpu核心并行运行。
CPU有一组寄存器,存放内存数据的拷贝。但是cpu处理速度远远高于内存,首先从缓存中读取数据,读取不到再从内存中读取。

java内存模型与硬件内存架构的关系

对于硬件内存只有寄存器,缓存内存,主内存的概念,并没有工作内存和主内存之分。
java内存模型只是一种抽象概念,不管是工作内存和主内存都存储再计算机内存中。
全面理解Java内存模型(JMM)及volatile关键字01_第4张图片

Java内存模型的特性

JMM内存模型出现的问题

原子性

问题:如果存在多个线程同时对基本类型的变量进行操作,他们的读写是会存在相互干扰的

理解指令重排

计算机在执行程序时,为了提高性能,编译器和处理器常常会对指令进行重排

  • 编译器优化重排:不改变单线程语义前提下,重新安排语句的执行顺序
  • 处理器优化重排:处理器采用指令级并行技术使得多条指令重叠执行
  • 内存优化重排:处理cpu和内存时间差

编译器重排例子

线程 1             线程 2
1: x2 = a ;      3: x1 = b ;
2: b = 1;         4: a = 2 ;

编译器对这段程序代码执行重排优化后

线程 1              线程 2
2: b = 1;          4: a = 2 ; 
1:x2 = a ;        3: x1 = b ;

结论:在多线程环境下,编译器优化重排,会导致结果不一致问题

处理器重排例子

class MixedOrder{
    int a = 0;
    boolean flag = false;
    public void writer(){
        a = 1;
        flag = true;
    }

    public void read(){
        if(flag){
            int i = a + 1}
    }
}
 线程A                    线程B
 writer:                 read:
 1:flag = true;           1:flag = true;
 2:a = 1;                 2: a = 0 ; //误读
                          3: i = 1 ;

由于指令重排的原因,线程a的flag置为true提前执行了。
指令重排只会保证单线程串行语义的一致性,但并不会关心多线程间的语义一致性

有序性

有序性是指按顺序执行。
编译器优化还是处理器优化重排,都会导致程序执行顺序问题

可见性

可见性值得是当一个线程修改了某个共享变量的值,其他线程是否能够马上得知这个修改的值
在多线程环境下,共享变量都是拷贝到工作内存进行操作的,然后再写回主内存。
工作内存与主内存同步延迟问题造成了可见性问题。
编译器优化还是处理器优化重排,都会导致程序执行顺序问题,间接导致可见性问题。

JMM提供的解决方案

  1. JVM自身提供对基本数据类型读写操作的原子性
  2. 方法级别,代码级别的原子性操作,可以使用synchronized关键字或者重入锁(ReentrantLock)保证程序执行的原子性
  3. 工作内存与主内存导致的可见性问题,可以使用synchronized关键字或者volatile关键字解决,它们都可以使一个线程修改后的变量立即对其他线程可见
  4. 指令重排导致的可见性问题和有序性问题,则可以利用volatile关键字解决

理解JMM中的happens-before原则

仅用sybchronized和volatile关键字保证原子性,可见性,有序性,那么编写并发程序可能显得十分麻烦

作用:在Java内存模型中,还提供了happens-before 原则来帮助程序员判断数据是否存在竞争、线程是否安全

具体七项规则

在现代操作系统上编写并发程序时,除了要注意线程安全性问题(多个线程互斥访问临界资源)以外,还要注意多线程对共享变量的可见性

  1. 程序顺序规则:对一个线程每个操作,happens-before后续对它的操作
  2. 传递性规则:a happens-before b,b happens-before c,那么a happens-before c
  3. 锁规则:对一个锁的解锁,happens-before后续对它的加锁
  4. volatile变量规则:对volatile域的写,happens-before后续对它的读
  5. 启动规则:若线程A启动线程B,那么启动操作 happens-before 线程B中的任意操作
  6. join规则:若线程A执行join 线程B,线程B中任意操作 happens-before join操作
  7. 中断规则:对线程的interrupted处理 happens-before 被中断线程检测中断的发生

上述8条原则无需手动添加任何同步手段(synchronized|volatile)即可达到效果
下面我们结合前面的案例演示这8条原则如何判断线程是否安全

class MixedOrder{
    int a = 0;
    boolean flag = false;
    public void writer(){
        a = 1;
        flag = true;
    }

    public void read(){
        if(flag){
            int i = a + 1}
    }
}

线程A调用实例对象的writer方法,线程B调用实例对象的read方法,线程A先启动,线程B后启动。
存在两线程同时调用,程序次序原则不满足。
两方法都没有使用同步手段,锁规则不满足。
没有使用volatile,volatile变量原则不满则
上述代码没有适合8条原则中的任意一条,也没有使用任何同步手段,所以上述的操作是线程不安全的,因此线程B读取的值自然也是不确定的
解决:要么给writer()方法和read()方法添加同步手段,如synchronized
给变量flag添加volatile关键字,确保线程A修改的值对线程B总是可见

分析volatile内存语义 通过内存屏障实现

volatile关键字的两个作用

  • 保证volatile共享变量对所有线程是立即可见的但并不保证实现线程安全
  • 禁止指令重排序优化

volatile的可见性

但是对于volatile变量运算操作在多线程环境并不保证安全性

public class VolatileVisibility {
    public static volatile int i =0;

    public static void increase(){
        i++;
    }
}

i++;操作并不具备原子性,该操作是先读取值,然后写回一个新值,相当于原来的值加上1,分两步完成
因此对于increase方法必须使用synchronized修饰,以便保证线程安全,需要注意的是一旦使用synchronized修饰方法后,由于synchronized本身也具备与volatile相同的特性,即可见性,因此在这样种情况下就完全可以省去volatile修饰变量。

public class VolatileVisibility {
    public static int i =0;

    public synchronized static void increase(){
        i++;
    }
}

使用volatile修饰变量达到线程安全目的

public class VolatileSafe {

    volatile boolean close;

    public void close(){
        close=true;
    }

    public void doWork(){
        while (!close){
            System.out.println("safe....");
        }
    }
}

由于对于boolean变量close值的修改属于原子性操作,因此可以通过使用volatile修饰变量close,使用该变量对其他线程立即可见

内在机制:
当写一个volatile变量时,JMM会把该线程对应的工作内存中的共享变量值刷新到主内存中,
当读取一个volatile变量时,JMM会把该线程对应的工作内存置为无效,那么该线程将只能从主内存中重新读取共享变量

volatile禁止指令重排序

内存屏障,又称内存栅栏,是一个CPU指令
作用有两:

  1. 保证特定操作的执行顺序
  2. 保证某些变量内存可见性 从而实现volatile内存可见性
    由于编译器和处理器都能执行指令重排优化。如果在指令间插入一条Memory Barrier则会告诉编译器和CPU,不管什么指令都不能和这条Memory Barrier指令重排序,也就是说通过插入内存屏障禁止在内存屏障前后的指令执行重排序优化

总结

JMM就是一组规则,这组规则意在解决在并发编程可能出现的线程安全问题,并提供了内置解决方案(happen-before原则)及其外部可使用的同步手段(synchronized/volatile等),确保了程序执行在多线程环境中的应有的原子性,可视性及其有序性

你可能感兴趣的:(JVM虚拟机)