一、基础概念
1.cpu多级缓存
1) 为什么需要cpu缓存
cpu频率太快,主存跟不上,cpu在处理问题时,需要等待主存,浪费资源。缓存(cache)的出现缓解了cpu和内存之间速度不匹配问题(cpu->cache->memory)。
2) 缓存的意义
时间局部性(如果某个数据被的访问,在不久的将来还可能被访问)。
空间局部性(如果某个数据被访问,相邻的数据很快也可能被访问)。
3)cpu多级缓存的缓存一致性(MESI协议)
用于保证多个cpu cache之间缓存共享数据的一致。
4)cpu多级缓存-乱序执行优化
处理器为提高运算速度而做出违背代码原有顺序的优化。
在多处理器的时代可能会带来各种各样的问题,如在一个内核上写入一个数据并给它一个标记用来表示这个数据是否已经准备好,在另一个内核上通过该标记判断数据时候已经就绪。这时候就可能发生标记先生成而数据写入操作后完成/并未完成/还没有从处理器缓存刷新到主存中,导致另一个内核使用了错误的数据。
2.Java内存模型(Java Memory Model,JMM)
JMM定义了Java 虚拟机(JVM)在计算机内存(RAM)中的工作方式。JVM是整个计算机虚拟模型,所以JMM是隶属于JVM的。
1)作用
规定了一个线程如何&何时可以看到其他线程修改过的共享变量的值以及在必须时如何同步的访问共享变量(线程间的通信和同步)。
2)JVM对Java内存模型的实现
在JVM内部,Java内存模型把内存分成了两部分,线程栈区和堆区。
堆(Heap):
堆区包含了Java应用创建的所有对象信息,不管对象是哪个线程创建的,其中的对象包括原始类型的封装类(如Byte、Integer、Long等等)。不管对象是属于一个成员变量还是方法中的本地变量,它都会被存储在堆区。
当两个线程的线程栈中都有指向同一个对象的引用,那么这两个线程都拥有该对象中成员变量的私有拷贝。
由垃圾回收来负责管理,可以动态的分配内存大小,有生存周期。
栈(Stack):
JVM中运行的每个线程都拥有自己的线程栈,线程栈包含了当前线程执行的方法调用相关信息,我们也把它称作调用栈,随着代码的不断执行,调用栈会不断变化。
线程栈还包含了当前方法的所有本地变量信息。一个线程只能读取自己的线程栈,也就是说,线程中的本地变量对其它线程是不可见的。即使两个线程执行的是同一段代码,它们也会各自在自己的线程栈中创建本地变量,因此,每个线程中的本地变量都会有自己的版本。
所有原始类型(boolean,byte,short,char,int,long,float,double)的本地变量都直接保存在线程栈当中,对于它们的值各个线程之间都是独立的。对于原始类型的本地变量,一个线程可以传递一个副本给另一个线程,它们之间是无法共享的。
下图展示了调用栈和本地变量都存储在栈区,对象都存储在堆区:
一个本地变量如果是原始类型,那么它会被完全存储到栈区。
一个本地变量也有可能是一个对象的引用,这种情况下,这个本地引用会被存储到栈中,但是对象本身仍然存储在堆区。
对于一个对象的成员方法,这些方法中包含本地变量,仍需要存储在栈区,即使它们所属的对象在堆区。
对于一个对象的成员变量,不管它是原始类型还是包装类型,都会被存储到堆区。
Static类型的变量以及类本身相关信息都会随着类本身存储在堆区。
堆中的对象可以被多线程共享。如果一个线程获得一个对象的应用,它便可访问这个对象的成员变量。如果两个线程同时调用了同一个对象的同一个方法,那么这两个线程便可同时访问这个对象的成员变量,但是对于本地变量,每个线程都会拷贝一份到自己的线程栈中。
3)硬件内存架构
现代计算机一般都有2个以上CPU,而且每个CPU还有可能包含多个核心。因此,如果我们的应用是多线程的话,这些线程可能会在各个CPU核心中并行运行。
在CPU内部有一组CPU寄存器,也就是CPU的储存器。CPU操作寄存器的速度要比操作计算机主存快的多。在主存和CPU寄存器之间还存在一个CPU缓存,CPU操作CPU缓存的速度快于主存但慢于CPU寄存器。某些CPU可能有多个缓存层(一级缓存和二级缓存)。计算机的主存也称作RAM,所有的CPU都能够访问主存,而且主存比上面提到的缓存和寄存器大很多。
当一个CPU需要访问主存时,会先读取一部分主存数据到CPU缓存,进而在读取CPU缓存到寄存器。当CPU需要写数据到主存时,同样会先flush寄存器到CPU缓存,然后再在某些节点把缓存数据flush到主存。
4)Java内存模型和硬件架构之间的桥接
Java内存模型和硬件内存架构并不一致。硬件内存架构中并没有区分栈和堆,从硬件上看,不管是栈还是堆,大部分数据都会存到主存中,当然一部分栈和堆的数据也有可能会存到CPU寄存器中,如下图所示,Java内存模型和计算机硬件内存架构是一个交叉关系。
5)Java内存模型抽象结构图
线程A与线程B之间通信:首先,线程A把本地内存A中更新过的共享变量刷新到主内存中。
然后,线程B到主内存中读取线程A更新过的共享变量。
3.同步的八种操作
1)lock(锁定):作用于主内存变量,把一个变量标识为一条线程独占状态。
2)unlock(解锁):作用于主内存变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
3)read(读取):作用于主内存变量,把一个变量值从主内存传输到线程的工作内存中,以便之后的load操作使用。
4)load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的副本中。
5)use(使用):作用与工作内存变量,把工作内存中的一个变量值传递给引擎执行。
6)assign(赋值):作用于工作内存的变量,把一个从执行引擎接收到的值赋值给工作内存中的变量。
7)store(存储):作用与工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的write操作。
8)write(写入):作用于主内存的变量,它把store操作从工作内存中一个变量的值传送到主内存的变量中。
4.并发编程的风险和优势
1)优势:速度、设计、资源利用
2)风险:安全性、活跃性、性能