
在Java并发知识体系中JMM是一个非常重要的概念掌握JMM可以让我们对多线程编程的理解更进一步。一、JMM是什么JMM ,Java Memory Model,中文含义为Java内存模型。主要用于对多线程环境下内存操作的一系列规范也可以称之为协议。二、为什么会出现JMM现代计算机结构对提升同时出现了一些副作用JMM的出现就是为了解决这些问题。JMM就是从这个模式下抽象出来的一套模型并根据缓存一致性协议来规范多线程间的内存共享和通信。1、现代计算机结构主要为了解决CPU和物理内存速度上的巨大差异而引入了CPU缓存环节速度差的问题引入了CPU缓存这个中间层。当中物理内存被称为主内存CPU缓存被称为工作内存。工作内存只供本CPU使用其他CPU不能读写。数据读取流程是工作内存读取主内存的数据CPU的寄存器读取工作内存的数据。如果CPU读取工作内存中找不到对应的数据则工作内存向主内存进行读取后CPU再读取。但是工作内存的大小有限如果对于那些很大的数据CPU缓存无法存储CPU才向主内存直接读取。2、工作内存细化工作缓存CPU高速缓存可以分为三级:L1、L2、L3从左到右速度越慢、空间越大、离CPU越远对于多核CPU来说L1和L2是该核CPU独享L3是则是同一个CPU内部所有核模块共享的。3、缓存行工作内存向主内存读取数据例如读取属性A的内存数据时预测近期会对内存A附近的数据进行访问因此为了提升整体效率就把连同A一起整块内存数据进行读取这块内存数据称之为缓存行。为了避免各个厂商自身对缓存行的定义不已特别是大小方面因此统一标准每个缓存行的大小为64个字节。4、出现的问题上述结构在单线程的情况下解决了CPU和物理内存之间频率的差异导致以及频繁占用I/O总线带宽等这两个问题的性能瓶颈使得计算机的运算能力得到质的飞跃。但是在多线程的环境下却会造成诸多问题如缓存一致性问题、指令重排导致结果无法预测。下面具体讲述这两个问题缓存一致性问题从上面描述可以指导CPU使用的是共享变量的副本多个CPU之间会对副本值进行修改然后回写到主内存。以那个CPU的回写值为准这个就需要CPU对缓存的操作遵循某些协议但是不同的系统的协议也是有所差异。指令重排问题为了提升运行效率程序指令在三处的进行重新排序。第一种编译器编译器根据单线程环境下语义不被改变的情况下重新安排指令的执行顺序。例如以下两条语句调整顺序不改变执行效果。ab; c2;第二种CPU执行时如果指令间不存在数据依赖则CPU会通过并行运行去执行这些指令。那么那些指令存在数据依赖呢主要有以下三种形式类型实例描述写后读a10;ba;写一个变量后再读该变量的值写后写a10;b20;写一个变量后再改变该变量的值读后写ab;b1;读取一个变量的值后再改变该变量的值大家也可以看出处在数据依赖的两个语句如果更改运行顺序那么计算结果就会改变。第三种内存系统CPU使用缓存和读写缓存冲区load和store这两个原子操作执行看起来混乱会让主内存与CPU缓存的同步出现时间差。三、JMM如何工作首先我们了解JMM的规范内容JMM规定了所有的变量都存储在主内存Main Memory中。每个线程还有自己的工作内存Working Memory,线程的工作内存中保存了该线程使用到的变量的主内存的副本拷贝线程对变量的所有操作读取、赋值等都必须在工作内存中进行而不能直接读写主内存中的变量volatile变量仍然有工作内存的拷贝但是由于它特殊的操作顺序性规定所以看起来如同直接在主内存中读写访问一般。不同的线程之间也无法直接访问对方工作内存中的变量线程之间值的传递都需要通过主内存来完成。总的来说线程只可以直接操作工作内存然后让工作内存与主内存进行数据同步。其次JMM以Java并发编程的三大特征原子性、可见性、有序性为基础建立的。1原子性要求一个操作不能被打断要么全部执行完毕要么不执行。JMM同步的八种操作,每种都是原子操作操作含义lock(锁定)作用于主内存的变量把一个变量标记为一条线程独占状态unlock(解锁)作用于主内存的变量把一个处于锁定状态的变量释放出来释放后的变量才可以被其他线程锁定read(读取)作用于主内存的变量把一个变量值从主内存传输到线程的工作内存中以便随后的load动作使用load(载入)作用于工作内存的变量它把read操作从主内存中得到的变量值放入工作内存的变量副本中use(使用)作用于工作内存的变量把工作内存中的一个变量值传递给执行引擎assign(赋值)作用于工作内存的变量它把一个从执行引擎接收到的值赋给工作内存的变量store(存储)作用于工作内存的变量把工作内存中的一个变量的值传送到主内存中以便随后的write的操作write(写入)作用于工作内存的变量它把store操作从工作内存中的一个变量的值传送到主内存的变量中这八种操作使用规定的使用规则1不允许read和load、store和write操作之一单独出现。read与loadstore与write必须成对使用2不允许线程丢弃他最近的assign操作。即工作变量的数据改变了之后必须告知主存3不允许一个线程将没有assign的数据从工作内存同步回主内存。即store前必须有assign。4一个新的变量必须在主内存中诞生不允许工作内存直接使用一个未被初始化的变量。即use前必须有load。5一个变量同一时间只有一个线程能对其进行lock。指多次lock后必须执行相同次数的unlock才能解锁6如果对一个变量进行lock操作会清空所有工作内存中此变量的值在执行引擎使用这个变量前必须重新load或assign操作初始化变量的值7如果一个变量没有被lock就不能对其进行unlock操作。也不能unlock一个被其他线程锁住的变量8对一个变量进行unlock操作之前必须把此变量同步回主内存2可见性要求当有线程对共享变量的修改要马上被其他的线程立即感知到。在计算机架构中CPU对经过总线的数据进行监听被称之为CPU嗅探。当CPU工作内存向主内存进行数据同步时需要经过总线那么总线就会记录下数据值然后被其他CPU所监听到如果涉及自身工作内存内的共享变量变化时一旦主内存值被更新其他CPU马上把工作内存中的对应缓存行设置为无效。下次使用到相关共享变量时需要重新从主内存中读取并加载到本地工作内存中。不过工作内存更新主内存的操作由CPU决定并一定马上执行因此可能CPUA准备同步主内存时等待CPU调度时CPUB抢先更新主内存这时候CPUA的工作内存就被强制设置为无效让A的这次计算白费从而有可能导致整个程序结果异常。Java提供一个关键字volatile被该关键字修饰的变量变更新后马上从工作内存同步到主内存中保证多线程下变量的可见性。3有序性有序性指多线程执行的结果和单线程条件下执行的结果一致。但是程序指令经过编译器、内存系统以及执行器的指令重排多线程下会出现有序性被破坏的情况。Java中synchronized关键字可以保证修饰的代码范围内指令有序性。而内部机制方面happen-before原则可以保证多并发中的原子性、可见性以及有序性内容如下1程序次序规则Program Order Rule在一个线程内按照控制流顺序书写在前面的操作先行发生Happens-before于书写在后面的操作。注意这里说的是控制流顺序而不是程序代码顺序因为要考虑分支、循环等结构。2管程锁定规则Monitor Lock Rule一个 unlock 操作先行发生于后面对同一个锁的 lock 操作。这里必须强调的是 “同一个锁”而 “后面” 是指时间上的先后。3volatile 变量规则Volatile Variable Rule对一个 volatile 变量的写操作先行发生于后面对这个变量的读操作这里的 “后面” 同样是指时间上的先后。4线程启动规则Thread Start RuleThread 对象的 start() 方法先行发生于此线程的每一个动作。5线程终止规则Thread Termination Rule线程中的所有操作都先行发生于对此线程的终止检测我们可以通过 Thread 对象的 join() 方法是否结束、Thread 对象的 isAlive() 的返回值等手段检测线程是否已经终止执行。(6线程中断规则Thread Interruption Rule对线程 interrupt() 方法的调用先行发生于被中断线程的代码检测到中断事件的发生可以通过 Thread 对象的 interrupted() 方法检测到是否有中断发生。(7对象终结规则Finalizer Rule一个对象的初始化完成构造函数执行结束先行发生于它的 finalize() 方法的开始。(8传递性Transitivity如果操作 A 先行发生于操作 B操作 B 先行发生于操作 C那就可以得出操作 A 先行发生于操作 C 的结论。四、总结JMM的出现是针对计算机架构优化产生让CPU缓存与物理内存不一致的问题而抽象出的一个逻辑内存模型。模型力求解决上述不一致的问题并为开发者屏蔽底层硬件、操作系统的差异问题为开发者通过统一的编程规范。