0 CPU 性能优化手段 - 缓存
为了提高程序运行的性能,现代CPU在很多方面对程序进行了优化。:
例如: CPU高速缓存。
尽可能地避免处理器访问主内存的时间开销,处理器大多会利用缓
存(cache)以提高性能。
0.1 多级缓存
- L1 Cache(一级缓存)
CPU第一层高速缓存,分为数据缓存和指令缓存。一般服务器CPU的L1缓存的容量通常在32- - 4096KB。 - L2
由于L1级高速缓存容量的限制,为了再次提高CPU的运算速度,在CPU外部放置一高速存
储器,即二级缓存。: - L3
现在的都是内置的。而它的实际作用即是,L3缓存的应用可以进一步降低内存延迟,同时
提升大数据量计算时处理器的性能。具有较大L3缓存的处理器提供更有效的文件系统缓存行为
及较短消息和处理器队列长度。-般是多核共享一-个L3缓存!
CPU在读取数据时,先在L1中寻找,再从L2寻找,再从L3寻找,然后是内存,再后是外存储器。
0.2 缓存同步协议
多CPU读取同样的数据进行缓存,进行不同运算之后,最终写入主内存以哪个CPU为准?
在这种高速缓存回写的场景下,有一个缓存一致性协议, 多数CPU厂商对它进行了实现。
MESI协议
,它规定每条缓存有个状态位,同时定义了下面四个状态:
- 修改态(Modified)
此cache行已被修改过(脏行),内容已不同于主存,为此cache专有; - 专有态(Exclusive)
此cache行内容同于主存,但不出现于其它cache中; - 共享态(Shared)一此cache行内容同于主存,但也出现于其它cache中;
- 无效态(Invalid)
此cache行内容无效(空行)
多处理器时,单个CPU对缓存中数据进行了改动,需要通知给其他CPU
这意味着,CPU处理要控制自己的读写操作,还要监听其他CPU发出的通知,从而保证最终
一致。
0.3 问题
缓存中的数据与主内存的数据并不是实时同步的,各CPU (或CPU核心)间缓存的数据也不是
实时同步。在同一个时间点,各CPU所看到同一内存地址的数据的值可能是不一致的。
1 Java内存模型(JMM)的意义
内存模型描述程序的可能行为。
Java虚拟机规范中试图定义一种Java内存模型来屏蔽掉各种硬件和操作系统的内存访问差异
,规定
- 线程如何,何时能看到其他线程修改过的共享变量的值
- 在必要时如何同步地访问共享变量
以实现让Java程序在各种平台下都能达到一致性的内存访问效果。
Java编程语言内存模型 通过检查执行跟踪中的每个读操作,并根据某些规则检查该读操作观察到的写操作是否有效来工作。
只要程序的所有执行产生的结果都可以由内存模型预测。具体的实现者任意实现,包括操作的重新排序和删除不必要的同步。
内存模型决定了在程序的每个点上可以读取什么值
1.1 Shared Variables 共享变量的描述
可以在线程之间共享的内存称为共享内存或堆内存
所有实例字段、静态字段和数组元素都存储在堆内存中
如果至少有一个访问是写的,那么对同一个变量的两次访问(读或写)是冲突的。
2 主内存与工作内存
- 工作内存缓存
- Java内存模型的主要目标是定义
各个变量的访问规则
即在虚拟机中将变量存储到内存和从内存中取出变量值这样的底层细节
此处的变量
包括了实例域,静态域和构成数组对象的元素,但不包括局部变量与方法参数,因为后者是线程私有的,不存在竞争
为了获得比较好的执行效率,JMM并没有限制执行引擎使用处理器的特定寄存器或缓存来和主内存进行交互,也没有限制即时编译器调整代码执行顺序这类权限。
JMM规定
- 所有的变量都存储在主内存(Main Memory)
- 每条线程有自己的工作内存(Working Memory)
保存了该线程使用到的变量的主内存副本拷贝
(线程所访问对象的引用或者对象中某个在线程访问到的字段,不会是整个对象的拷贝!)
线程对变量的所有操作(读,赋值等)都必须在工作内存中进行,不能直接读写主内存中的变量
volatile变量依然有工作内存的拷贝,只是他特殊的操作顺序性规定,看起来如同直接在主内存读写
不同线程之间无法直接访问对方工作内存中的变量,线程间变量值的传递均要通过主内存
JVM模型与JMM不是同一层次的内存划分,基本是没有关系的,硬要对应起来,从变量,内存,工作内存的定义来看
- 主内存 === Java堆中的对象实例数据部分
- 工作内存 === 虚拟机栈中的部分区域
从更底层的层次来说
- 主内存直接对应于物理硬件的内存
- 为了更好的运行速度,虚拟机(甚至硬件系统的本身的优化措施)可能会让工作内存优先存储于寄存器和高速缓存器中,因为程序运行时主要访问读写的是工作内存
3 内存间同步操作
3.1 线程操作的定义
操作定义
write要写的变量以及要写的值。
read要读的变量以及可见的写入值(由此,我们可以确定可见的值)。
lock要锁定的管程(监视器monitor)。
unlock要解锁的管程。
外部操作(socket等等…)
启动和终止
程序顺序
如果一个程序没有数据竞争,那么程序的所有执行看起来都是顺序一致的
本规范只涉及线程间的操作;
一个变量如何从主内存拷贝到工作内存,从工作内存同步回主内存的实现细节
JMM 本身已经定义实现了以下8种操作来完成,且都具备原子性
-
lock(锁定)
作用于主内存变量,把一个变量标识为一条线程独占的状态 -
unlock(解锁)
作用于主内存变量,把一个处于锁定状态的变量释放,释放后的变量才可以被其它线程锁定
unlock之前必须将变量值同步回主内存 -
read(读取)
作用于主内存变量,把一个变量的值从主内存传输到工作内存,以便随后的load -
load(载入)
作用于工作内存变量,把read从主内存中得到的变量值放入工作内存的变量副本 -
use(使用)
作用于工作内存变量,把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到的变量的值得字节码指令时将会执行这个操作 -
assign(赋值)
作用于工作内存变量,把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作 -
store(存储)
作用于工作内存变量,把工作内存中一个变量的值传送到主内存,以便随后的write操作使用 -
write(写入)
作用于主内存变量,把store操作从工作内存中得到的值放入主内存的变量中 -
把一个变量从主内存
复制
到工作内存
就要顺序执行read和load -
把变量从工作内存
同步
回主内存
就要顺序地执行store和write操作
JMM只要求上述两个操作必须按序执行
,而没有保证连续执行
也就是说read/load之间、store/write之间可以插入其它指令
如对主内存中的变量a,b访问时,一种可能出现的顺序是read a->readb->loadb->load a
JMM规定执行上述八种基础操作时必须满足如下
3.1 同步规则
◆ 对于监视器 m 的解锁与所有后续操作对于 m 的加锁 同步
(之前的操作保持可见)
◆对 volatile变量v的写入,与所有其他线程后续对v的读同步
◆ 启动
线程的操作与线程中的第一个操作同步
◆ 对于每个属性写入默认值(0, false, null)与每个线程对其进行的操作同步
◆ 线程 T1的最后操作与线程T2发现线程T1已经结束同步。( isAlive ,join可以判断线程是否终结)
◆ 如果线程 T1中断了T2,那么线程T1的中断操作与其他所有线程发现T2被中断了同步通过抛出InterruptedException异常,或者调用Thread.interrupted或Thread.isInterrupted
- 不允许read/load、store/write操作之一单独出现
不允许一个变量从主内存读取了但工作内存不接收,或从工作内存发起回写但主内存不接收 - 不允许一个线程丢弃它的最近的assign
即变量在工作内存中改变(为工作内存变量赋值)后必须把该变化同步回主内存 - 新变量只能在主内存“诞生”,不允许在工作内存直接使用一个未被初始化(load或assign)的变量
换话说就是一个变量在实施use,store之前,必须先执行过assign和load - 如果一个变量事先没有被load锁定,则不允许对它执行unlock,也不允许去unlock一个被其它线程锁定的变量
- 对一个变量执行unloack前,必须把此变量同步回主内存中(执行store,write)