JMM (Java memory model)JAVA 内存模型
它是一种规范,它规范了Java 虚拟机与计算机内存是如何协同工作的,它规定了线程如何和何时可以看到其他线程修改过后的共享变量的值,以及在必需时,如何同步地访问共享变量。
JVM 内存分配 堆和栈:
堆
一个运行时的数据区,它是由垃圾回收来负责的;
优点
可以动态分配内存大小,生存期也不必事前告诉编译器(因为在运行时动态分配大小,java垃圾收集器会自动回收不再使用的数据)
缺点
由于在运行时动态分配内容,因此,存取速度相对较慢;
栈 Stack
优点
存取速度比堆较快,仅次于CPU的寄存器,栈内的数据是可以共享的;
缺点
栈中的数据大小必须在生存取是确定的,缺乏一定的灵活性,主要存储一些基本类型数据类型(int,short,byte... )变量和对象句柄;
JMM要求
要求调用栈和本地变量存放在线程栈上,对象存放在堆上。一个本地变量也可能是指向一个对象的引用,这种情况下,引用这个本地变量存放在线程栈上,但是,对象本身是存放在堆上的;
一个对象可能包含方法methodOne,methodTwo,Z这些方法中可能含有本地变量localVarible 1, localVarible 2,这些本地变量仍然是存放在线程栈上的,即使这方法所属的对象存储在堆上;
一个对象的成员变量,可能会随着这个这个对象自身而存放堆上,不管这个成员变量是原始原始类型还是引用类型;
静态成员变量跟随类着类的定义,一起存放在在堆上;
存放在堆上的对象可以被所持有这个对象引用的线程访问,比如访问此处的object3,当一个线程,可以访问一个对象的时候,也可以访问这个对象的成员变量,如果两个线程同时调用一个对象上的同一个方法,他们都会访问这个对象的成员变量,但是,每一个线程都拥有了这个成员变量的私有拷贝(特别重要);
计算机硬件架构简单图示
多核CUP
多核cpu上运行多个线程,如果你的 java 代码是多线程的,那么你的程序在多核cpu下是并发执行。
内部寄存器
每个 cpu 都包含一系列的寄存器,它们是 cpu 内存的基础,cpu 在寄存器上执行的速度远大于在主存上执行的速度,这是因为 cpu 访问寄存器的速度远大于主存。
高速缓存 cache
由于计算机的存储设备和处理器的运算速度之间有几个数量级的差异,因此,计算机不得不在寄存器和主存之间加入一个存取速率尽可能接近寄存器的高速缓存,来作为内存与处理器之间的缓冲,把运算用到的数据复制到缓存中,让运算快速的执行,当运算结束后,再从缓存同步到内存之中,这样,处理器就无需等待缓慢的内存读写了 ,cpu 访问缓存的速度快于访问主存的速度,比访问内部寄存器的速度还是要慢一点。每个 cpu 可能有一个 cpu 缓存层,一个 cpu 含有多层缓存,某一时刻,一个或多个缓存行可能被读到缓存,一个或多个缓存行可能被刷新回主存。
主存(内存)
所有的 cpu 都可以访问主存,通常比 cpu 中的缓存大的多,
它们的运作原理
通常情况下,需要读取主存的时候,会将主存中的数据读取到 cpu 缓存中,甚至会将 cpu 缓存中的数据读取到 cpu 内部寄存器中,然后在寄存器中执行操作。当 cpu 需要将结果回写到主存中时,它会将内部寄存器的值刷新到缓存中,然后再某个时间点,把值刷新回主存。
Java 内存模型和硬件内存架构之间的一些关联
Java 内存模型和硬件内存架构之间是存在一些差异的,硬件内存架构没有区分堆和栈,对硬件而言,所有的堆和栈都分配在主内存当中,部分栈和堆可能出现在 cpu 缓存中、cpu 的内部寄存器中
从抽象的角度来看线程和主内存之间的抽象关系
线程之间的共享变量存放在主内存之中,每个线程都有一个私有的本地内存,本地内存是 JMM 的一个抽象的概念,并不是真实存在的,它涵盖了缓存、写缓冲区、寄存器以及其他的一些硬件和编译器的优化。本地内存中存储了该线程已读或写共享变量的一个拷贝的副本。从更低的层次来说,主内存就是硬件的内存,是为了获取更好的运行速度,虚拟机及硬件系统可能会把工作内存优先存储于高速缓存和内部寄存器中,Java 内存模型线程的工作内存是 cpu 的寄存器和高速缓存的一个抽象的描述,而 JVM 静态内存存储模型它只是一种对内存的物理划分而已,它只局限在内存,而且,只局限在 JVM 的内存
线程A与线程B之间通信的步骤
线程A需要把本地内存中更新过的共享变量刷新到主内存之中去;
线程B去主内存中去读取线程A已经更新过的共享变量;
这里来说一个多线程计数问题 count ++,我们假设 count 为 1 操作:线程A首先从主内存中读取共享变量 count 到本地内存A中,执行加一操作,此时,count的值为 2;在线程A读取count到自己的本地内存A的同时,线程B也开始读取count到自己的本地内存B中(而不是读取线程A更新过的值),开始执行加一操作;随后,线程A开始把自己的运算结果 2 刷回到主存中,在此同时,线程B也刷新自己的计算结果刷新到主存中,由于线程A和线程B之间的操作是相互不可见的,因此,计数就出现了错误,必须添加一些同步的手段来保证并发时,程序处理的准确性。
Java 内存模型-同步8种操作及规则
过程操作
Lock 锁定 - 作用于主内存变量,把一个变量标识为一条线程独占的状态;
Unlock 解锁 - 作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定;
Read 读取 - 作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的 load 动作使用;
Load 载入- 作用于工作内存变量,它把 read 操作从主内存中得到的变量值放入工作内存的变量副本中;
Use 使用 - 作用与工作内存变量,把工作内存中的一个变量值传递给执行引擎;
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 和 unloack 必须成对出现;
如果一个变量执行 lock 操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量之前需要重新执行 load 或 assign 操作初始化变量的值;
如果一个变量实现没有被 lock 操作锁定,则不允许对它进行 unlock 操作;也不允许去 unlock 一个被其他线程锁定的变量;
对一个变量执行 unlock 操作之前,必须先把此变量同步到主内存中(执行 store 和 write 操作);
并发的优势和风险