在单核计算机中,计算机中的CPU计算速度是很是快的,可是与计算机中的其它硬件(如IO、内存等)同CPU的速度比起来是相差甚远的,因此协调CPU和各个硬件之间的速度差别是很是重要的,要否则CPU就一直在等待,浪费资源。单核尚且如此,在多核中,这样的问题会更加的突出。硬件结构以下图所示:java
咱们先大概梳理下这个流程:当咱们的计算机要执行某个任务或者计算某个数字时,主内存会首先从数据库中加载计算机计算所须要的数据,由于内存和CPU的速度相差较大,因此有必要在内存和CPU间引入缓存(根据实际的须要,能够引入多层缓存),主内存中的数据会先存放在CPU缓存中,当这些数据须要同CPU作交互时会加入到CPU寄存器中,最后被CPU使用。程序员
事实上,在单核状况下,基于缓存的交互能够很好的解决CPU与其它硬件之间的速度匹配,可是在多核状况下,各个处理器都要遵循必定的协议来保障内存中的各个处理器的缓存和主内存中的数据一致性问题,这类协议一般被称为缓存一致性协议。面试
咱们在开发时会常常遇到这样的场景,咱们开发完成的代码在咱们本身的运行环境上表现良好,可是当咱们把它放在其它硬件平台上时,就会出现各类各样的错误,这是由于在不一样的硬件生产商和不一样的操做系统下,内存的访问逻辑有必定的差别,结果就是当你的代码在某个系统环境下运行良好,而且线程安全,可是换了个系统就出现各类问题。数据库
为了解决这个问题,Java内存模型(JMM)的概念就被提出来了,它的出现能够屏蔽系统和硬件的差别,让一套代码在不一样平台下能到达相同的访问结果,实现平台的一致性,使得Java程序可以一次编写,处处运行。编程
这样的描述的好像有点熟悉啊,这不是JVM的概念描述么,它们二者有什么区别啊?segmentfault
JVM与JMM间的区别?缓存
实际上,JMM是Java虚拟机(JVM)在计算机内存(RAM)中的工做方式,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每一个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本,本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器以及其余的硬件和编译器优化。而JVM则是描述的是Java虚拟机内部及各个结构间的关系。安全
小伙伴这时可能会有疑问,既然JMM是定义线程和主内存之间的关系,那么它的出现是否是解决并发领域的问题啊?没错,咱们先回顾一下并发领域中的关键问题。markdown
并发领域中的关键问题?多线程
在编程中,线程之间的通讯机制有两种,共享内存
和消息传递
。
在共享内存的并发模型里,线程之间共享程序的公共状态,线程之间经过写-读内存中的公共状态来隐式进行通讯,典型的共享内存通讯方式就是经过共享对象进行通讯。
消息传递的并发模型里,线程之间没有公共状态,线程之间必须经过明确的发送消息来显式进行通讯,在java中典型的消息传递方式就是wait()和notify()。
同步是指程序用于控制不一样线程之间操做发生相对顺序的机制。 在共享内存并发模型里,同步是显式进行的。程序员必须显式指定某个方法或某段代码须要在线程之间互斥执行。 在消息传递的并发模型里,因为消息的发送必须在消息的接收以前,所以同步是隐式进行的。
事实上,Java内存模型(JMM)的并发采用的是共享内存模型。
下面,咱们一块儿来学习Java内存模型
咱们先看一张JMM的控制模型做图
因而可知,Java内存模型(JMM)同CPU缓存模型结构相似,是基于CPU缓存模型来创建的。
咱们先梳理一下JMM的工做流程,以上图为例,咱们假设有一台四核的计算机,cpu1操做线程A,cpu2操做线程B,cpu3操做线程C,当这三个线程都须要对主内存中的共享变量进行操做时,这三条线程分别会将主内存中的共享内存读入本身的工做内存,本身保存一份共享变量的副本供本身线程自己使用。
这时有的小伙伴可能会有如下疑问:
主内存、工做内存的定义是什么?
如何将主内存中的共享变量读入本身线程自己的工做内存?
当其中的某一条线程修改了共享变量后,其他线程中的共享变量值是否变化,若是变化,线程间是怎么保持可见性的?
下面,咱们针对这两个疑问一一解答。
主内存主要存储的是Java实例对象,即全部线程建立的实例对象都存放在主内存中,无论该实例对象是成员变量仍是方法中的本地变量(也称局部变量),固然也包括了共享的类信息、常量、静态变量。因为是共享数据区域,多条线程对同一个变量进行访问可能会发现线程安全问题。
工做内存主要存储当前方法的全部本地变量信息(工做内存中存储着主内存中的变量副本拷贝),即每一个线程只能访问本身的工做内存,即线程中的本地变量对其它线程是不可见的,就算是两个线程执行的是同一段代码,它们也会各自在本身的工做内存中建立属于当前线程的本地变量,固然也包括了字节码行号指示器、相关Native方法的信息。注意因为工做内存是每一个线程的私有数据,线程间没法相互访问工做内存,所以存储在工做内存的数据不存在线程安全问题。
**NOTE:**这里的主内存、工做内存与Java内存区域中的Java堆、栈、方法区不是同一层次的内存划分,这二者基本上没有关系。
搞清楚主内存和工做内存后,下一步就须要学习主内存与工做内存的数据交互操做的方式。
主内存与工做内存的交互操做有8种,虚拟机必须保证每个操做都是原子的,这八种操做分别是:
做用于主内存的变量,把一个变量标识为一条线程独占状态。
做用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才能够被其余线程锁定
做用于主内存变量,它把一个变量的值从主内存传输到线程的工做内存中,以便随后的load动做使用
做用于工做内存的变量,它把read操做从主存中变量放入工做内存中
做用于工做内存中的变量,它把工做内存中的变量传输给执行引擎,每当虚拟机遇到一个须要使用到变量的值,就会使用到这个指令
做用于工做内存中的变量,它把一个从执行引擎中接受到的值放入工做内存的变量副本中
做用于主内存中的变量,它把一个从工做内存中一个变量的值传送到主内存中,以便后续的write使用
做用于主内存中的变量,它把store操做从工做内存中获得的变量的值放入主内存的变量中
单看这八种类型的原子操做可能有点抽象,咱们画一个操做流程图仔细梳理下。
操做流程图:
从图中能够看出,若是要把一个变量从内存中复制到工做内存中,就须要顺序的执行read和load操做,若是把变量从工做内存同步到主内存中,就须要执行store和write操做。
NOTE: Java内存模型只要求上述操做必须按顺序执行,却没要求是连续执行。
咱们以两个线程为例梳理下操做流程:
假设存在两个线程A和B,若是线程A要与线程B要通讯的话,首先,线程A把本地内存A中更新过的共享变量刷新到主内存中去;而后,线程B到主内存中读取线程A以前已经更新过的共享变量。
敏锐的小伙伴可能会发现,若是多个线程同时读取修改同一个共享变量,这种状况可能会致使每一个线程中的本地内存中缓存变量一致的问题,这个时候该怎么解决呢?
解决JMM中的本地内存变量的缓存不一致问题有两种解决方案,分别是总线加锁
和MESI缓存一致性协议
。
总线加锁
总线加锁是CPU从主内存读取数据到本地内存时,会先在总线对这个数据加锁,这样其它CPU就无法去读或者去写这个数据,直到这个CPU使用完数据释放锁后,,其它的CPU才能读取该数据。
总线加锁虽然能保证数据一致,可是它却严重下降了系统性能,由于当一个线程多总线加锁后,其它线程都只能等待,将原有的并行操做转成了串行操做。
一般状况下,咱们不采用这种方法,而是使用性能较高的缓存一致性协议。
MESI缓存一致性协议
MESI缓存一致性协议是多个CPU从主内存读取同一个数据到各自的高速缓存中,当其中的某个CPU修改了缓存里的数据,该数据会立刻同步回主内存,其它CPU经过总线嗅探机制能够感知到数据的变化从而将本身缓存里的数据失效。
在并发编程中,若是多个线程对同一个共享变量进行操做是,咱们一般会在变量名称前加上关键在volatile
,由于它能够保证线程对变量的修改的可见性,保证可见性的基础是多个线程都会监听总线。即当一个线程修改了共享变量后,该变量会立马同步到主内存,其他线程监听到数据变化后会使得本身缓存的原数据失效,并触发read
操做读取新修改的变量的值。进而保证了多个线程的数据一致性。事实上,volatile
的工做原理就是依赖于MESI缓存一致性协议实现的。
在Java多线程中,Java提供了一系列与并发处理相关的关键字,好比volatile
、synchronized
、final
、concurren
包等。其实这些就是Java内存模型封装了底层的实现后提供给程序员使用的一些关键字
事实上,Java内存模型的本质是围绕着Java并发过程当中的如何处理原子性
、可见性
和顺序性
这三个特征来设计的,这三大特性能够直接使用Java中提供的关键字实现,它们也是面试中常常被问到的题目。
原子性的定义是一个操做不能被打断,要么所有执行完毕,要么不执行。在这点上有点相似于事务操做,要么所有执行成功,要么回退到执行该操做以前的状态。
JMM保证的原子性变量操做包括read、load、assign、use、store、write
NOTE:基本类型数据的访问大都是原子操做,long 和double类型的变量是64位,可是在32位JVM中,32位的JVM会将64位数据的读写操做分为2次32位的读写操做来进行,这就致使了long、double类型的变量在32位虚拟机中是非原子操做,数据有可能会被破坏,也就意味着多个线程在并发访问的时候是线程非安全的。
对于非原子操做的基本类型,可使用synchronized来保证方法和代码块内的操做是原子性的。
1 synchronized (this) {
2 a=1;
3 b=2;
4 }
复制代码
如一个线程观察另一个线程执行上面的代码,只能看到a、b都被赋值成功结果,或者a、b都还没有被赋值的结果。
Java内存模型是经过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值的这种依赖主内存做为传递媒介的方式来实现的。
Java中的volatile关键字提供了一个功能,那就是被其修饰的变量在被修改后能够当即同步到主内存,被其修饰的变量在每次是用以前都从主内存刷新。所以,可使用volatile来保证多线程操做时变量的可见性。
除了volatile,Java中的synchronized和final两个关键字也能够实现可见性。只不过实现方式不一样,这里再也不展开了。
在Java中,可使用synchronized和volatile来保证多线程之间操做的有序性。实现方式有所区别:
volatile关键字会禁止指令重排。synchronized关键字保证同一时刻只容许一条线程操做。
好了,这里简单的介绍完了Java并发编程中解决原子性、可见性以及有序性可使用的关键字。读者可能发现了,好像synchronized关键字是万 能的,他能够同时知足以上三种特性,这其实也是不少人滥用synchronized的缘由。
可是synchronized是比较影响性能的,虽然编译器提供了不少锁优化技术,可是也不建议过分使用。
参考文献
[1]https://www.jianshu.com/p/8a58d8335270 [2]https://blog.csdn.net/javazejian/article/details/72772461 [3]https://blog.csdn.net/zjcjava/article/details/78406330 [4]https://segmentfault.com/a/1190000016085105