# 一、硬件内存模型 ![[1742867-20191104115215072-2033982939.jpg]] 将运算需要使用到的数据复制到缓存中,让运算能够快速进行。当运算完成之后,再将缓存中的结果写入主内存,这样运算器就不用等待主内存的读写操作了。 每个处理器都有自己的高速缓存,同时又共同操作同一块主内存,当多个处理器同时操作主内存时,可能导致数据不一致,因此需要“缓存一致性协议”来保障。 # 二、java内存模型 Java内存模型即Java Memory Model,简称JMM。**用来屏蔽掉各种硬件和操作系统的内存访问差异,以实现让Java程序在各平台下都能够达到一致的内存访问效果。** ## 2.1 并发编程存在的问题: 1. 原子性,是指在一个操作中就是**cpu不可以在中途暂停然后再调度**,既不被中断操作,要么执行完成,要么就不执行。 2. 可见性,是指当多个线程访问同一个变量时,一个线程修改了这个变量的值,其他线程能够立即看得到修改的值。 3. 有序性,程序执行的顺序按照代码的先后顺序执行。 **缓存一致性问题**其实就是**可见性问题**。而**处理器优化**(处理器可能会对输入代码进行乱序执行处理)是可以导致**原子性问题**的。**指令重排**(Java虚拟机的即时编译器(JIT)也会做**指令重排**)即会导致**有序性问题** ### 2.1.1 原子性 在Java中,为了保证原子性,提供了两个高级的字节码指令`monitorenter`和`monitorexit`。在[synchronized的实现原理](http://www.hollischuang.com/archives/1883)文章中,介绍过,这两个字节码,在Java中对应的关键字就是`synchronized`。 因此,在Java中可以使用`synchronized`来保证方法和代码块内的操作是原子性的。 ### 2.1.2 可见性(多线程计数器实现) Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值的这种依赖主内存作为传递媒介的方式来实现的。 Java中的`volatile`关键字提供了一个功能,那就是被其修饰的变量在被修改后可以立即同步到主内存,被其修饰的变量在每次是用之前都从主内存刷新。因此,可以使用`volatile`来保证多线程操作时变量的可见性。 除了`volatile`,Java中的`synchronized`和`final`两个关键字也可以实现可见性。只不过实现方式不同,这里不再展开了。 ### 2.1.3 有序性 在Java中,可以使用`synchronized`和`volatile`来保证多线程之间操作的有序性。实现方式有所区别: `volatile`关键字会禁止指令重排。`synchronized`关键字保证同一时刻只允许一条线程操作。 好了,这里简单的介绍完了Java并发编程中解决原子性、可见性以及有序性可以使用的关键字。读者可能发现了,好像`synchronized`关键字是万能的,他可以同时满足以上三种特性,这其实也是很多人滥用`synchronized`的原因。但是`synchronized`是比较影响性能的,虽然编译器提供了很多锁优化技术,但是也不建议过度使用。 ## 2.2 JMM详解 ![[1742867-20191104115215072-2033982939 1.jpg]] JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(main memory)中,每个线程都有一个私有的本地内存(local memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的一个抽象概念,并不真实存在。它涵盖了缓存,写缓冲区,寄存器以及其他的硬件和编译器优化。 JMM与Java内存结构并不是同一个层次的内存划分,两者基本没有关系。如果一定要勉强对应,那从变量、主内存、工作内存的定义看,主内存主要对应Java堆中的对象实例数据部分,工作内存则对应虚拟机栈的部分区域。 ![[1742867-20191104115215072-2033982939 2.jpg]] ### 2.2.1 主内存 主要存储的是Java实例对象,所有线程创建的实例对象都存放在主内存中,不管该实例对象是成员变量还是方法中的本地变量(也称局部变量),当然也包括了共享的类信息、常量、静态变量。共享数据区域,多条线程对同一个变量进行访问可能会发现线程安全问题。 ### 2.2.2 工作内存 要存储当前方法的所有本地变量信息(工作内存中存储着主内存中的变量副本拷贝),每个线程只能访问自己的工作内存,即线程中的本地变量对其它线程是不可见的,就算是两个线程执行的是同一段代码,它们也会各自在自己的工作内存中创建属于当前线程的本地变量,当然也包括了字节码行号指示器、相关Native方法的信息。由于工作内存是每个线程的私有数据,线程间无法相互访问工作内存,因此存储在工作内存的数据不存在线程安全问题。 ### 2.2.3 模型简化 JMM模型与硬件模型直接的对照关系可简化为下图: ![[1742867-20191104115215072-2033982939 3.jpg]] ### 2.2.4 内存之间的交互操作 线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作都必须在工作内存中进行,而不能直接读写主内存中的变量。不同的线程之间也无法直接访问对方工作内存中的变量(隔离性),线程间变量值的传递均需要通过主内存来完成。 ![[1742867-20191104115215072-2033982939 4.jpg]] 如上图,本地内存A和B有主内存中共享变量x的副本,初始值都为0。线程A执行之后把x更新为1,存放在本地内存A中。当线程A和线程B需要通信时,线程A首先会把本地内存中x=1值刷新到主内存中,主内存中的x值变为1。随后,线程B到主内存中去读取更新后的x值,线程B的本地内存的x值也变为了1。 举个例子:两个人(可以理解为2个线程),向SVN提交一个相同的类,但是内容不同。先提交的人SVN回复OK。后提交的人,SVN显示OUT OF DATE,此时后提交的人需要先更新自己的代码(基于前人的基础)然后再提交。 在此交互过程中,Java内存模型定义了8种操作来完成,虚拟机实现必须保证每一种操作都是原子的、不可再拆分的(double和long类型例外)。 - lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。 - unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。 - read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。 - load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。 - use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。 - assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。 - store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用。 - write(写入):作用于主内存的变量,它把store操作从工作内存中得到的变量的值放入主内存的变量中。 ### 2.2.5 半个变量 ava内存模型要求lock,unlock,read,load,assign,use,store,write这8个操作都具有原子性,但对于64位的数据类型(long或double),在模型中定义了一条相对宽松的规定,允许虚拟机将没有被volatile修饰的64位数据的读写操作划分为两次32位的操作来进行,即允许虚拟机实现选择可以不保证64位数据类型的load,store,read,write这4个操作的原子性,即long和double的非原子性协定。 如果多线程的情况下double或long类型并未声明为volatile,可能会出现“半个变量”的数值,也就是既非原值,也非修改后的值。 虽然Java规范允许上面的实现,但商用虚拟机中基本都采用了原子性的操作,因此在日常使用中几乎不会出现读取到“半个变量”的情况。 ### 2.2.6 总结 JMM与COW(copy-on-write)机制很像,都从统一内存(存储)读取,当需要修改的时候复制一份到自己本地并副本进行修改。