美文网首页
JVM内存模型

JVM内存模型

作者: 扎Zn了老Fe | 来源:发表于2020-03-21 00:10 被阅读0次

1.JMM模型

Java内存模型和多处理器计算机系统有着很多相似之处:



缓存一致性:每条线程都有自己的工作内存,里面保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,不能直接读写主内存中的变量,相似的,Java虚拟机也定义了一套内存访问协议来保证内存一致性;

指令重排序:对应于处理器乱序执行,Java虚拟机的即时编译器中也有着类似的优化,同样只保证最终结果的一致性。

内存一致性:每条线程都有自己的工作内存,里面保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,不能直接读写主内存中的变量,相似的,Java虚拟机也定义了一套内存访问协议来保证内存一致性;

指令重排序:对应于处理器乱序执行,Java虚拟机的即时编译器中也有着类似的优化,同样只保证最终结果的一致性。

2.volatile

volatile是Java虚拟机提供的最轻量的同步机制,但很难被正确的理解与使用,通过学习Java内存模型对volatile专门定义的一些特殊访问规则,或许会对理解volatile有一定帮助。
当一个变量被定义为volatile之后,将会具备两种特性:
可见性:当一条线程改变了该变量的值,新值对所有其他线程来说都是可以立即感知的;
禁止指令重排序优化:volatile变量赋值完成语句之后的指令不会被重排序到该赋值指令之前;

2.1可见性

大多数开发人员最容易误解的是volatile的可见性特性,会从volatile变量在各个线程中是一致的,错误的推导出基于volatile变量的运算也是并发安全的,下面一段代码对此进行了实验

private static volatile int sum = 0;

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

    /**
     * 使用10跟线程对sum各累加10000次,理论上返回值为100000
     */
    public static void test() {
        try {
            Thread[] threads = new Thread[10];
            for (int i = 0; i < 10; ++i) {
                threads[i] = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        for (int i = 0; i < 10000; ++i) {
                            increase();
                        }
                    }
                });
                threads[i].start();
            }
            //等待所有线程执行完毕
            for (int i = 0; i < 10; ++i) {
                threads[i].join();
            }
            System.out.println("sum = " + sum);
        } catch (Exception e) {
            //ignore
        }
    }

    public static void main(String[] args) {
        //实验10次
        for (int i = 0; i < 10; ++i) {
            sum = 0;
            test();
        }
    }

这是因为,Java中的运算(++sum)并不是原子操作。
volatile使用场景
1.运算结果并不依赖变量的当前值(如作为状态变量),或仅有单一线程修改变量值

//当shutdown()被调用时,所有线程的doWork()都会立刻停止
private volatile boolean isShutdown = false;
public void shutdown(){
  isShutdown=true;
}
public void doWork(){
  while(!isShutdown){
    //anything
  }
}

2.变量不与其他状态变量共同参与不变约束(因为对其中一个变量值赋值之后并在对其他变量赋值之前,不变约束可能失效)
如果不能同时满足上述两个条件,则必须使用锁来保证并发安全。

2.2禁止指令重排序优化

在介绍volatile的禁止指令重排序优化之前,首先要了解一下线程内表现为串行的语义(Within-Thread As-If-Serial Semantics)和内存屏障(Memory Barrier)。
内存屏障:指令重排序时不能把内存屏障之后的指令重排序到内存屏障之前。
线程内表现为串行的语义:普通的变量仅仅会保证在该方法的执行过程中所有依赖赋值结果的地方都能获取到正确的结果,而不能保证变量赋值的操作顺序与程序代码中执行的顺序一致,这在单线程的方法执行过程中是无法感知的,因而表现为线程内串行。

3. 原子性、可见性和有序性

并发编程的场景中的三个bug源头:可见性、原子性、有序性
1.可见性:一个线程对共享变量的修改,另外一个线程能够立即看到,我们称为可见性。多核系统每个cpu自带高速缓存,彼此间不交换信息(例子:两个线程对同一份实列变量count累加,结果可能不等于累加之和,因为线程将内存值载入各自的缓存中,之后的累加操作基于缓存值进行,并不是累加一次往内存回写一次)。

2.原子性:我们把一个或者多个操作在cpu执行的过程中不被打断的特性称为原子性,CPU能保证的原子操作是CPU执行级别的。高级语言的一条语句对应底层多条CPU执行指令,cpu分时操作导致线程的切换,多条CPU执行指令的执行被打断,对高级语言来说就不具备原子性了。(例子:AB两个线程同时进行count+=1,由于+=操作是3步指令①从内存加载②+1操作③回写到主内,线程A对其进行了①②操作后,切换到B线程,B线程进行了①②③,这时内存值是1,然后再切到A执行③操作,这时的值也还是1,PS:这貌似也存在可见性的问题)

3.有序性:指令的重排序 (例子:单列模式的双重检测,new指令也是3步操作,①分内存②初始化③赋值给引用变量,可能会发生①③②的重排序,这时候如果又有操作系统的分时操作的加持,导致A操作①③后挂起,时间片被分配给了B线程,而B线程甚至都不需要进行锁的获取,因为此时instance已经不等于null了,但是此时的instance可能未初始化)。

4. happen-before原则

以代码为例

class VolatileExample {
    int x = 0;
    volatile boolean v = false;

    public void writer() {
        x = 42;
        v = true;
    }

    public void reader() {
        if (v == true) {
            // 这里 x 会是多少呢?
        }
    }
}

4.1 程序的顺序性规则

指在一个线程中,按照代码顺序,前面的操作Happens-Before后续任意操作。比如上述代码x=42就发生在v=true之前,所以x变量对后续操作是可见的。

4.2 volatile变量原则

指一个volatile变量的写操作,Happens-Before后续对这个变量的读操作。关联3.3的规则。

4.3 传递性

指A Happens-Before B, B Happens-Before C, 那么A Happens-Before C。


image.png

从图中看到:

x=42 Happens-Before v=true, 这是规则1;
写变量v=true Happens-Before 读变量v=true,这是规则2;
根据规则3,x=42 Happens-Before 读变量v=true, 换句话说,线程B可以读到x=42,x对线程B是可见的。

4.4. 管程中的锁原则

指的是:对一个锁的解锁Happens-Before后续对这个锁的加锁。
管程:一种通用的同步术语,在java中的实现是synchronized。
管程中的锁在java中是隐式实现的,例如下面的代码:进入代码块,自动加锁;执行完代码块自动释放锁。加锁和释放锁都是编译器实现的。

synchronized (this) { // 此处自动加锁
  // x 是共享变量, 初始值 =10
  if (this.x < 12) {
    this.x = 12; 
  }  
} 

结合此规则:线程A执行完代码块,x=12,线程A释放锁;此时线程B获得锁,线程B能看到线程A对x的写操作,即B看到x=12。

4.5. 线程start()规则

指的是:主线程A启动子线程B,那么子线程B可以看到主线程启动子线程B之前的操作。

Thread B = new Thread(()->{
  // 主线程调用 B.start() 之前
  // 所有对共享变量的修改,此处皆可见
  // 此例中,var==77
});
// 此处对共享变量 var 修改
var = 77;
// 主线程启动子线程
B.start();

4.6. 线程join()规则

指的是:主线程A等待子线程B完成(A调用B的join()方法),B完成后,A能看到B的操作。

Thread B = new Thread(()->{
  // 此处对共享变量 var 修改
  var = 66;
});
// 例如此处对共享变量修改,
// 则这个修改结果对线程 B 可见
// 主线程启动子线程
B.start();
B.join()
// 子线程所有对共享变量的修改
// 在主线程调用 B.join() 之后皆可见
// 此例中,var==66

满足原子性:synchorized(锁)
满足可见性:synchorized(根据happen-before 锁原则获得), volatile, final
满足有序性:votatile, synchorized(一个变量同一时刻只允许被一个线程lock)

image.png
image.png

/

相关文章

  • JVM内存模型(jvm 入门篇)

    概述 jvm 入门篇,想要学习jvm,必须先得了解JVM内存模型,JVM内存模型,JVM内存模型,JVM内存模型,...

  • JVM

    栈容量由-Xss指定深入理解JVM—JVM内存模型 JVM内存模型和JVM参数的关系

  • [Java多线程编程之八] Java内存模型

    一、Java内存模型 == JVM内存模型?   很多人都会认为Java内存模型就是JVM内存模型,但实际上是错的...

  • JVM问题及解答

    常见JVM问题 JVM内存模型,GC机制和原理。注意JVM内存模型与Java内存模型(JMM)不是同一个东西。JV...

  • JVM内存结构和Java内存模型

    最近看到两个比较容易混淆的概念:JVM内存结构和Java内存模型 JVM内存结构JVM内存结构或者说内存模型指的是...

  • 高效并发

    从JVM的角度看一下Java与线程,内存模型,线程安全以及JVM对于锁的优化 硬件内存模型与JVM内存模型 硬件的...

  • jvm

    1.5.1JVM的内存模型 首先我们来了解一下JVM的内存模型的怎么样的: 基于jdk1.8画的JVM的内存模型-...

  • JVM基础知识点

    1. 内存模型以及分区,需要详细到每个区放什么(共分为5个)。 JVM内存模型及分区jvm内存模型和内存分配 程序...

  • 面试系列之JVM

    1.jvm内存模型 jvm内存模型主要有运行时期模型和非运行时期两部分组成,通常说的jvm内存模型是指运行时期内存...

  • jvm内存模型

    Java虚拟机内存模型 计划发布3篇博客, 这是第一篇:jvm内存模型 jvm内存模型 对象创建和内存分配 OOM...

网友评论

      本文标题:JVM内存模型

      本文链接:https://www.haomeiwen.com/subject/rbdwxhtx.html