Java内存模型(JMM)
 
调用栈和本地变量放在线程栈上,对象放在堆上。
  1. 变量,无论它是原始类型还是对象中的本地变量,它都放在线程栈上。但如果变量是对象的成员变量则可能放在堆上也可能在线程栈上。如果是静态成员变量就随着类定义一起放在堆上。
  2. 引用(属于变量,是对堆上的对象的引用)也放在线程栈上
  3. 存放在堆上的对象可以被所有持有对这个对象引用的线程访问。当一个线程可以访问一个对象时,它也可以访问这个对象的成员变量。如果两个线程同时调用同一个对象上的同一个方法,它们将会都访问这个对象的成员变量,但是每一个线程都拥有这个成员变量的私有拷贝。
 
 
 
 
 
 
而  硬件内存架构
 
多CPU:
意味着Java程序如果是多线程的, 每个CPU一个线程的时候,多CPU计算机可以实现线程的并行(同时)执行。
并发:多个事务在同一时刻发生; 并行:多个事务在同一时间间隔发生
 
CPU寄存器-----高速缓存Cache-------内存(主存)
 
运行原理:
 
 
全写法(写直达法):
写操作时数据既写入Cache又写入主存
写操作时间就是访问主存的时间,Cache块退出时,不需要对主存执行写操作,更新策略比较容易实现。
 
写回法:
写操作时只把数据写入Cache而不写入主存,当Cache数据被替换出去时才写回主存。
写操作时间就是访问Cache的时间,Cache块退出时,被替换的块需写回主存,增加Cache的复杂性。
 
 
写分配法:和写回法搭配
未命中则先将数据从主存调入Cache,然后对Cache进行写操作,最后用写回法将Cache中的数据写回主存。
 
非写分配法:和直写法搭配
CPU直接写入主存,不与Cache交互。
 
 
 
 
 
 
多线程下的缓存一致性问题
 
在多处理器系统中,每个处理器都有自己的高速缓存,而它们又共享同一主内存(MainMemory)。
基于高速缓存的存储交互很好地解决了处理器与内存的速度矛盾,但是也引入了新的问题:缓存一致性(CacheCoherence)。
当多个处理器的运算任务都涉及同一块主内存区域时,将可能导致各自的缓存数据不一致的情况。
如果真的发生这种情况,那同步回到主内存时以谁的缓存数据为准呢?为了解决一致性的问题,需要各个处理器访问缓存时都遵循一些协议,在读写时要根据协议来进行操作,这类协议有MSI、MESI(IllinoisProtocol)、MOSI、Synapse、Firefly及DragonProtocol,等等
 
 
多线程下的指令重排序问题
 
为了使得处理器内部的运算单元能尽量被充分利用,处理器可能会对输入代码进行乱序执行(Out-Of-Order Execution)优化,
处理器会在计算之后将乱序执行的结果重组,保证该结果与顺序执行的结果是一致的,但并不保证程序中各个语句计算的先后顺序与输入代码中的顺序一致。
因此,如果存在一个计算任务依赖另一个计算任务的中间结果,那么其顺序性并不能靠代码的先后顺序来保证。与处理器的乱序执行优化类似,Java虚拟机的即时编译器中也有类似的指令重排序(Instruction Reorder)优化。
 
  
Java内存模型和硬件内存架构之间的桥接
 
从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:
  •  
  • 线程之间的共享变量存储在主内存(Main Memory)中
  •  
  • 每个线程都有一个私有的本地内存(Local Memory),本地内存是JMM的一个抽象概念,并不真实存在,它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。本地内存中存储了该线程以读/写共享变量的拷贝副本。
  •  
  • 从更低的层次来说,主内存就是硬件的内存,而为了获取更好的运行速度,虚拟机及硬件系统可能会让工作内存优先存储于寄存器和高速缓存中。
  •  
  • Java内存模型中的线程的工作内存(working memory)是cpu的寄存器和高速缓存的抽象描述。而JVM的静态内存储模型(JVM内存模型)只是一种对内存的物理划分而已,它只局限在内存,而且只局限在JVM的内存。(区分好Java内存模型中的线程的工作内存  和  JVM内存模型  之间的区别,一个是抽象描述一个是内存的物理划分)
 
 
 
 
 
JMM模型下的线程间通信
 
   线程间通信必须要经过主内存。
如下,如果线程A与线程B之间要通信的话,必须要经历下面2个步骤:
1)线程A把本地内存A中更新过的共享变量刷新到主内存中去。
2)线程B到主内存中去读取线程A之前已更新过的共享变量。
关于一个变量如何从主内存拷贝到工作内存、如何从工作内存同步到主内存之间的实现细节,Java内存模型定义了以下八种操作来完成:
  • lock锁定:作用于主内存的变量,把一个变量标识为一条线程独占状态
  • unlock解锁:作用于主内存变量,把一个处在锁定状态的内存释放出来
  • read读取:作用于主内存变量,将其读取到线程的工作内存
  • load载入:作用于工作内存变量,把上面read来的变量放入工作内存的变量副本中
  • use使用:作用于工作内存变量,吧工作内存中的变量值传递给执行引擎。每当虚拟机遇到一个需要使用变量的值的字节码指令的时候就会执行这个操作
  • assign赋值:作用于工作内存变量,把一个引擎接收的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令的时候就会执行这个操作
  • store存储:作用于工作内存变量,将变量的值传送到主内存
  • write写入:作用于主内存变量,将上一步store操作的变量值传到主内存变量中
 
一定要按顺序执行,但不一定要连续执行。
 
 
Java内存模型解决的问题
 
 
    1.多线程读同步与可见性
 
可见性(同步共享对象可见性):线程对共享变量修改的可见性。当一个线程修改了共享变量的值,其他线程能够立刻得知这个修改
 
现存缓存导致的可见性问题:
下图示意了这种情形。跑在左边CPU的线程拷贝这个共享对象到它的CPU缓存中,然后将count变量的值修改为2。这个修改对跑在右边CPU上的其它线程是不可见的,因为修改后的count的值还没有被刷新回主存中去。
 
 
Volatile、synchronized、final关键字可以解决。
  • volatile:保证直接从主存中读变量,修改后一定会写回主存
  • synchronized:如果对变量lock,就一定清空工作内存中此变量的值,unlock之前要先把变量同步回主存
  • final:如果字段在构造器中被初始化而没有把他的引用传递出去,那么其他线程就可以看见final字段的值
 
重排序导致的可见性:
volatile:关键词禁止重排序
synchronized:规则“一个变量同一时刻只允许一个线程对他lock”
 
指令系统的重排序:
编译器优化的重排序
指令级并行(ILP Instuctuion-level Parellelism)的重排序
内存系统的重排序
 
数据依赖:
重排序时,会遵循数据依赖性,不会改变存在数据依赖关系的两个操作的执行顺序
 
 
as-if-serial语义:
不管怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变。(编译器、runtime和处理器都必须遵守as-if-serial语义)
 
happens before:
 
 
 
 
 
    2.多线程写同步与原子性
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
posted on 2022-08-11 13:22  Jolyne123  阅读(101)  评论(0)    收藏  举报