【Java并发编程】Java内存模型

Java内存模型

一、JMM解析

之前写过一篇文章【Java核心技术卷】谈谈对Java平台的理解,其中讨论“Java跨平台”的篇幅占了大半的位置,JVM的重要性不言而喻。

为了能够屏蔽各种硬件以及对操作系统的内存访问的差异,而且要能使得Java程序在各个平台下都能达到一致的并发效果。JVM规范中定义了Java的内存模型(Java Memory model, JMM)。

JMM是一种规范,它规范了JVM与计算机内存是如何协同工作的,规定了一个线程如何以及何时看到其他线程修改过的共享变量的值以及在必须时如何同步地访问到共享变量。


【Java并发编程】Java内存模型
下面我们来认识认识JMM,首先看一下规范下的JVM的内存分配。Heap为堆,Stack 为栈。

堆是一个运行时的数据区,也是Java的垃圾回收器重点关注的对象。堆的优势在于可以动态分配内存的大小,缺点是因为是运行时动态分配内存,存取速度要慢一点。

栈的优势存取的速度比堆要快,但是比计算机的寄存器要慢哈,栈的数据是可以共享的,但是存在栈中的数据的大小与生存期是确定的,灵活性较低,栈中主要存放一些基本类型的变量。比如int,short,long,byte,对象句柄等。

JMM要求调用栈和本地变量存放在线程栈上,对象存放在堆上。对了,一个对象可能包含方法,方法可能包含本地变量,这些本地变量仍然是存放在线程栈上的,即使这些对象拥有这些方法,也是要把这些对象放在堆中。

一个对象的成员变量随着对象存放在堆中,无论这些成员变量是原始类型还是引用类型。静态成员变量跟随类的定义一起存放在堆上,存放在堆上的对象可以被持有这个对象的引用的线程访问。线程通过引用访问这个对象的时候,也是能够访问这个对象的成员变量。如果多个线程同时调用同一个对象同一个方法,它们可能都将访问这个对象的成员变量,这个时候每个线程都会拥有相应的成员变量的私有拷贝
【Java并发编程】Java内存模型
私有拷贝,有疑惑吗?我这里演示一下吧

public class Main {
    
    public static void main(String[] args) {
        new Thread(() -> {
            Person person1 = new Person();
            person1.count();
            System.out.println(person1.getId());
        }).start();

        new Thread(() -> {
            Person person2 = new Person();
            person2.count();
            System.out.println(person2.getId());
        }).start();
    }
}

class Person {
    private Long id = 0L;
    public void count(){
        id++;
    }

    public Long getId() {
        return id;
    }
}

结果:
【Java并发编程】Java内存模型


上面是在JVM层次看多线程的。

下面看看硬件内存架构

二、硬件内存架构

【Java并发编程】Java内存模型
上面展示的是多个CPU,有个概念,这里需要点一下,你可千万别搞混了

多核CPU指的是一个CPU有多个CPU核心,多核CPU性能非常好,但成本较高;如果没钱,可以换为多个单核的CPU,如果有钱可以换成多个多核的CPU。

现在我们使用的计算机大都都是多个多核CPU了,这使得在实际使用的时候,会有多个CPU上都跑的有进程(线程),我们的Java程序如果是并发的,可能会在多个CPU上跑。

我们看一下CPU的寄存器,每个CPU都包含一系列的寄存器,它们是CPU内存的基础,寄存器执行的速度远大于主存上执行的速度,中间的缓存,我就不多说了,上篇文章介绍过了【Java并发编程】CPU多级缓存

CPU从主存中读取数据的时候,首先会将数据读取到缓存中,然后由缓存读取到寄存器中,然后再去执行,执行完步骤后,如果需要将结果写回到主存中,首先要将数据刷新到缓存中,缓存会在未来的某个时间点,将结果刷新到主存中。

三、JMM与硬件内存架构的关联

【Java并发编程】Java内存模型

Java内存模型与硬件架构模型之间是存在一些差异的,硬件架构模型没有区分线程栈与堆。对于硬件而言,所有的线程栈与堆都分配在主内存里面,部分线程栈和堆可能会出现在CPU的缓存中和CPU内部的寄存器中。

四、Java线程与计算机主内存之间的抽象关系

线程之间的共享变量存储在主内存里面,每一个线程都有一个私有的本地内存,本地内存是Java内存模型抽象的概念,并不是真实存在的,它涵盖了缓存、寄存器以及其他的硬件和编译器的优化等,本地内存中存储了该线程已读或写,共享变量拷贝的一个副本。Java内存模型的工作内存是CPU的寄存器和高速缓存的一个抽象的描述。Java内存模型的存储划分仅是是对其内部的物理划分而已,只局限在JVM的内存。

【Java并发编程】Java内存模型
由于每个线程都有自己的本地内存,它们如果同时访问主内存的共享变量,共享内存的值会分别copy到每个线程的本地变量中。每个线程对自己本地内存中的值做出的修改对其他线程都是不可见的,这个时候就会导致不一致性。

比如说主内存某个共享变量值为1,A和B线程都要对这个这个共享变量做出修改,A和B线程都先把值copy到自己的本地内存中,然后进行操作,A线程对其进行加1,并将值刷新到主内存中,B线程将其加2,但是相对于A线程慢了半拍,但是也成功将值刷新到主内存中。
此时,主内存中这个共享变量的值是3,当A再次从主内存中读取这个共享变量(中间会copy到它的本地内存),值已经不是2了。这个时候就导致了线程的安全性问题。

五、Java内存模型中同步八种操作

  1. lock(锁定):作用于主内存的变量,把—个变量标识为一条线程独占状态
  2. unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
  3. read(读取):作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
  4. load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中
  5. use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎
  6. assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作內存的变量
  7. store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的 write的操作
  8. write(写入):作用于主内存的变量,它把 store操作从工作内存中一个变量的值传送到主内存的变量中

【Java并发编程】Java内存模型

Lock作用于主内存的变量,它把一个变量标识为一个线程独占的状态,与其对应的就是unlock

Read读取,也是作用于主内存的变量,它变量的值从主内存变量输送到工作内存中(未到工作内存),与后边的load动作对接

Load是载入的意思,它将Read操作中变量的值放入工作内存的变量副本中

Use是使用,作用于工作内存中的变量, 它将工作内存中的变量传递给执行引擎,每当JVM遇到一个需要使用到的变量值的字节码指令的时候就会执行use这个操作。

Assign为赋值,作用于工作内存中的变量,它把从执行引擎接收到的值赋值给工作内存中的变量,每当JVM遇到一个需要给变量赋值的字节码指令的时候就会执行assign这个操作。

接下来是Store,也就是存储,它作用于工作内存中的变量,它将工作内存中的变量传递到主内存中(未到主内存),与后边的write操作对接

Write是写入的操作,它将Store操作中变量的值,放入到主内存的变量里面。

对应的同步规则有:

  • 如果要把一个变量从主内存中复制到工作内存,就需要按顺寻地执行read和load操作,如果把变量从工作内存中同步回主内存中,就要按顺序地执行 store和 write操作。但Java内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行
  • 不允许read和load、 store和 write操作之一单独出现
  • 不允许一个线程丢弃它的最近 assign的操作,即变量在工作内存中改变了之后必须同步到主内存中
  • 不允许一个线程无原因地(没有发生过任何 assign操作)把数据从工作内存同步回主内存中
  • 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或 assign)的变量。即就是对一个变量实施use和 store操作之前,必须先执行过了 assign和load操作
  • 一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的 unlock操作,变量才会被解锁。lock和 unlock必须成对出现
  • 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值
  • 如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去 unlock—个被其他线程锁定的变量
  • 对一个变量执行uηlock操作之前,必须先把此变量同步到主内存中(执行 store和 write操作)

Java并发相关的类设计时都遵循的规则,还有一些特殊的规则,之后再说。

上一篇:怎样启动 停止 重启MySQL数据库服务器


下一篇:【技巧】我是如何 "搜索" 到想要的信息的