1.为什么要有内存模型在现代的多核处理器中,每个处理器都有自己的缓存,需要定期与主存协调。确保每个处理器在任何时间点都知道其他处理器在做什么将是昂贵的,而且通常是不必要的。1.1硬件的效率和一致性1.由于计算机的存储设备和处理器的计算能力之间存在几个数量级的差距,现代计算机系统不得不加一层读写速度尽可能接近到处理器的计算速度。缓存(cache)作为内存和处理器之间的缓冲:将运算需要的数据复制到缓存中,以便运算快速执行,运算时再从缓存中同步回内存完全的。没有这样的处理器不需要等待缓慢的内存读写。2.多处理器计算任务涉及同一个主存,需要一个协议来保证数据的一致性。此类协议包括MSI、MESI、MOSI和Dragon协议。Java虚拟机内存模型中定义的内存访问操作类似于硬件缓存访问操作。3、基于缓存的存储交互解决了处理器速度和内存的冲突,但是引入了一个新的问题:缓存一致性(CacheCoherence)。在多处理器系统中,每个处理器都有自己的高速缓存,并且它们共享相同的主内存。下面将介绍这个问题。2.CPU与缓存的一致性2.1CPU频率太快,为什么需要CPU缓存?,快到主存跟不上,所以在处理器时钟周期内,CPU经常需要等待主存,浪费资源。CPU经常需要重复处理相同的数据,重复执行相同的指令。如果CPU能够在CPU缓存中找到这部分数据和指令,CPU就不需要从内存或硬盘中读取数据和指令,从而减少整体内存消耗。响应时间,所以缓存的出现是为了缓解程序执行过程中CPU和内存的速度不匹配(结构:cpu->缓存->内存)变成:程序运行时,会复制需要的数据从主存操作到CPU的缓存,那么CPU在执行计算时可以直接从它的缓存中读写数据。缓存中的数据被刷新到主存中。Intel官网产品-处理器接口中对缓存的定义是:CPU缓存是处理器上一块快速的内存区域。Intel智能缓存(SmartCache)是指一种架构,可以让所有核心动态共享末级缓存。这里提到最后一级缓存的概念,也就是CPU缓存中的L3(三级缓存),那我们继续解释什么是三级缓存,它指的是哪一级缓存。2.2三级缓存(L1、L2、L3)1)三级缓存(L1一级缓存、L2二级缓存、L3三级缓存)是集成在CPU中的缓存2)它们作为CPU和高速数据bufferbetweenthemainmemory3)L1距离CPU核心最近,L2次之,L3再次运行,从速度上看:L1最快,L2次之,L3最慢。Capacity:L1最小,L2最大,L3最大4)CPU会先在最快的L1中寻找需要的数据,如果找不到,就会寻找次快的L2,如果它找不到,那么它会寻找L3。如果没有L3,就只能到内存中找了。5)单核CPU只包含一组L1、L2、L3缓存;如果CPU包含多个核心,即一个多核CPU,每个核心都有一组L1(甚至有L2)缓存,同时共享L3(或有L2)缓存。单CPU双核缓存结构:在单线程环境下,CPU核的缓存只被一个线程访问。缓存是独占的,不会出现访问冲突等问题。在多线程场景下,如果在CPU和主存之间加入缓存,可能会出现缓存一致性问题。也就是说在多核CPU中,每个核都有自己的缓存2.3乱序执行优化从java源码到最终实际执行的指令序列,会经历以下三种重排序:重排序排序现象:a=10,b=a这组b依赖于a,不会重新排序a=10,b=50这组a和b没有任何关系,那么有可能重新排序执行b=50,a=10cpu编译器为了提高程序的执行效率,会允许指令按照一定的规则进行优化,这不会影响单线程程序的执行结果,但是多线程会影响程序结果。3.Java内存模型Java内存模型即JavaMemoryModel,简称JMM。JMM定义了Java虚拟机(JVM)在计算机内存(RAM)中的工作方式。JVM是整个计算机的虚拟模型,所以JMM属于JVM。Java内存模型(JavaMemoryModel,JMM)是一种符合内存模型规范,屏蔽各种硬件和操作系统的访问差异,保证Java程序在各种平台上访问内存的一致性。机制和规范。它可以避免直接使用物理硬件和操作系统(如c++)的内存模型,这些模型在不同的操作系统和硬件平台下表现不同。比如一些c/c++程序在windows平台上运行正常,但是在linux平台上运行就出现问题。注意JMM和JVM内存区域划分的区别:JMM描述了一套围绕原子性、顺序和可见性的规则;相同点:Java线程在共享区和私有区之间的通信采用的是over-sharingMemory模型,这里所说的共享内存模型是指JavaMemoryModel(简称JMM),JMM决定了线程何时写入共享变量对另一个线程可见。JMM从抽象的角度定义了线程与主存的抽象关系:线程间的共享变量存储在主存(mainmemory)中,每个线程都有一个私有的本地内存(localmemory),线程的一份副本读/写共享变量存储在本地内存中。本地内存是JMM的一个抽象概念,并不真正存在。它涵盖了高速缓存、写入缓冲区、寄存器以及其他硬件和编译器优化。从上图可以看出,线程A和线程B若要通信,必须经过以下两步:线程A将本地内存A中更新的共享变量刷新到主存中。线程B去主存读取之前线程A更新过的共享变量。具体示意图:如上图所示,本地内存A和B在主存中各有一份共享变量z。假设一开始,这三个内存中的z值都为0,当线程A在执行时,将更新后的z值(假设为1)暂存在自己的本地内存A中,当线程A和线程B需要时为了进行通信,线程A会先将自己本地内存中修改后的z值刷新到主存中,此时主存中的z值变为1,随后线程B去主存中读取更新后的z值线程A,此时线程B的本地内存的z值也变成了1。从整体上看,这两步本质上就是线程A向线程B发送消息,而这个通信过程必须经过主存。JMM通过控制主内存和每个线程的本地内存之间的交互,为Java程序员提供内存可见性保证。3.1JVM对Java内存模型的实现在JVM内部,Java内存模型将Java虚拟机分为:线程栈和堆线程栈:运行在Java虚拟机中的每个线程都有自己的线程栈。线程堆栈包含有关线程调用的方法的当前执行点的信息。一个线程只能访问它自己的线程栈。一个线程创建的局部变量对其他线程不可见,只对它自己可见。即使两个线程执行相同的代码,两个线程仍然会在自己的线程堆栈中的代码中创建局部变量。因此,每个线程都有每个局部变量的唯一版本。线程堆:堆包含Java程序中创建的所有对象,无论创建的是哪个对象。这包括原始类型的对象版本。如果一个对象被创建然后赋值给一个局部变量,或者作为另一个对象的成员变量使用,这个对象仍然存储在堆上。如果局部变量是原始类型,它将完全存储在堆栈中。局部变量也可以是对对象的引用。在这种情况下,本地引用将存储在堆栈中,但对象本身仍存储在堆中。对于一个对象的成员方法,这些方法包含局部变量,仍然需要存放在栈区,即使它们所属的对象在堆区。对于对象的成员变量,无论是原始类型还是包装类型,都会存放在栈区。堆区中Static类型的变量和类本身的相关信息,会和类本身一起存放在堆区中,可以被多个线程共享。如果一个线程获得了一个对象的应用,它就可以访问这个对象的成员变量。如果两个线程同时调用同一个对象的同一个方法,那么这两个线程可以同时访问这个对象的成员变量,但是对于局部变量,每个线程都会拷贝一份到自己的线程栈中3.2内存模型和硬件架构之间的Java桥接Java内存模型与硬件内存架构不一致。硬件内存架构中没有栈和堆的区别。从硬件的角度来看,无论是栈还是堆,大部分数据都会存放在主存中。当然,部分栈和堆数据也可能存放在CPU寄存器中,如下图所示,Java内存模型与计算机硬件内存架构是一种交叉关系:3.3Java内存模型——八种操作synchronized1)锁(lock):作用于主存的变量,标识一个变量为线程独占状态2)unock(解锁):作用于主存中的变量,释放一个处于锁定状态的变量,并且释放的变量可以被其他线程锁定3)读(read):作用于主存中的变量,释放一个变量的值,从主存转移到线程的工作内存,以便后续的加载动作可以使用4)加载(loading):作用于工作内存的变量,将读操作从主内存中获取的变量值放入工作内存中的变量副本5)使用(use):作用于工作记忆的变量,transfer将工作内存中的一个变量值赋给执行引擎6)赋值(assign):作用于工作内存的变量,它接收一个赋值给工作内存的变量7)存储(storage):作用于工作内存的变量,将工作内存中的一个变量的值转移到内存中,用于后续的写操作8)写入(write):作用于工作内存的变量,从变量的值转移存储操作3.4Java内存模型——同步规则如果要将一个变量从主存复制到工作内存,需要依次进行读取和加载操作。如果变量从工作内存同步回主内存,则存储和写入操作必须顺序执行。但是Java内存模型只要求申诉操作必须按顺序执行,并不能保证一定要连续执行。读取和加载、存储和写入操作之一不允许单独出现。不允许线程丢弃其最近分配的操作,即变量正在工作。内存更改后,必须同步到主内存。一个线程不允许无故(没有发生assign操作)将数据从工作内存同步回主内存。一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未初始化(加载或赋值)的变量。也就是说,在对一个变量实现use和store操作之前,必须先进行assign和load操作。一个变量只允许一个线程同时对其进行加锁操作,但加锁操作可以由同一个线程多次重复执行。第一次执行锁后,只有执行相同次数的解锁操作才会解锁变量。lock和unlock必须成对出现。如果对一个变量进行加锁操作,工作内存中该变量的值将被清空。执行引擎在使用这个变量之前,需要重新执行load或者assign操作来初始化这个变量的值。如果一个变量之前没有被使用过lock操作被锁定,则不允许对其执行解锁操作,也不允许解锁被其他线程锁定的变量在对变量执行解锁操作之前,变量必须同步到主存(执行store和write操作)原子性、可见性、有序性:可以查看我之前的文章:线程安全详解(原子性、可见性、有序性)四、并发的优势和风险优势:1)速度:用于处理多个请求和响应更快,复杂的操作可以分成多个进程同时执行2)设计:程序设计在某些情况下更简单,选择更多3)资源utilization:CPUcandowhilewaitingIO其他一些风险:1)Security:当多个线程共享数据时,可能会产生不符合预期的结果2)Liveness:Livenessissues当操作无法继续时发生。如死锁、饥饿等。3)性能:当线程过多时,会造成:频繁的CPU切换,增加调度时间;同步机制;内存消耗过大5.CPU多级缓存总结:缓存一致性、乱序执行、优化Java内存模型:JMM规则、抽象结构、同步的八种操作以及常规Java并发的优势和风险
