并发处理的广泛应用代替了摩尔定律成为计算机性能发展的源动力, 也是人类 "压榨" 计算机运算能力的最有力武器.
本文将介绍处理器的内存模型, JMM 即 Java 的内存模型, 和线程的安全性问题.
处理器的内存模型
由于计算机的存储设备与处理器的运算速度有几个数量级差距, 所以现代计算机系统都会加入一层读写速度尽可能接近处理器运算速度的高速缓存 (Cache) 来作为内存和处理器之间的缓冲, 将运算需要使用的数据复制到缓存中, 让运算能快速进行, 当运算结束后再从缓存同步到内存之中. 如下图所示:
高速缓存从下到上越接近 CPU 速度越快, 同时容量也越小. 现在大部分的处理器都有二级或者三级缓存, 从下到上依次为 L3 cache,L2 cache, L1 cache. 缓存又可以分为指令缓存和数据缓存, 指令缓存用来缓存程序的代码, 数据缓存用来缓存程序的数据.
L1 Cache, 一级缓存, 本地 core 的缓存, 分成 32K 的数据缓存 和 32k 指令缓存 , 访问 L1 需要 3cycles, 耗时大约 1ns;
L2 Cache, 二级缓存, 本地 core 的缓存, 大小为 256K, 访问 L2 需要 12cycles, 耗时大约 3ns;
L3 Cache, 三级缓存, 在同插槽的所有 core 共享 L3 缓存, 分为多个 2M 的段, 访问 L3 需要 38cycles, 耗时大约 12ns;
CPU 的术语定义
缓存一致性问题
基于高速缓存的存储交互很好地解决了处理器与内存的速度矛盾, 但是也为计算机系统带来更高的复杂度, 因为它引入了一个新的问题: 缓存一致性(Cache Coherence).
CPU-0 读取主存的数据, 缓存到 CPU-0 的高速缓存中, CPU-1 也做了同样的事情, 而 CPU-1 把 count 的值修改成了 2, 并且同步到 CPU-1 的高速缓存, 但是这个修改以后的值并没有写入到主存中, CPU-0 访问该字节, 由于缓存没有更新, 所以仍然是之前的值, 就会导致数据不一致的问题. 为了解决这个问题, CPU 生产厂商提供了相应的解决方案:
总线锁
当一个 CPU 对其缓存中的数据进行操作的时候, 往总线中发送一个 Lock 信号. 其他处理器的请求将会被阻塞, 那么该处理器可以独占共享内存. 总线锁相当于把 CPU 和内存之间的通信锁住了, 所以这种方式会导致 CPU 的性能下降, 所以 P6 系列以后的处理器, 出现了另外一种方式, 就是缓存锁.
缓存锁
如果缓存在处理器缓存行中的内存区域在 LOCK 操作期间被锁定, 当它执行锁操作回写内存时, 处理不在总线上声明 LOCK 信号, 而是修改内部的缓存地址, 然后通过缓存一致性机制来保证操作的原子性, 因为缓存一致性机制会阻止同时修改被两个以上处理器缓存的内存区域的数据, 当其他处理器回写已经被锁定的缓存行的数据时会导致该缓存行无效. 所以缓存锁会产生两个作用:
1, 将当前处理器缓存行的数据写回到系统内存.
2, 这个写回内存的操作会使在其他 CPU 里缓存了该内存地址的数据无效.
缓存一致性协议
处理器上有一套完整的协议, 来保证 Cache 的一致性. 比较经典的是 MESI 协议, 它的方法是在 CPU 缓存中保存一个标记位, 这个标记为有四种状态:
Ø M(Modified) 修改缓存
Ø I(Invalid) 失效缓存
Ø E(Exclusive) 独占缓存
Ø S(Shared) 共享缓存
每个 Core 的 Cache 控制器不仅知道自己的读写操作, 也监听其它 Cache 的读写操作, 嗅探 (snooping) 协议. CPU 的读取会遵循几个原则:
1, 如果缓存的状态是 I, 那么就从内存中读取, 否则直接从缓存读取
2, 如果缓存处于 M 或者 E 的 CPU 嗅探到其他 CPU 有读的操作, 就把自己的缓存写入到内存, 并把自己的状态设置为 S
Java 内存模型
Java 虚拟机规范中试图定义一种 Java 内存模型 (Java Memory Model,JMM) 来屏蔽掉各种硬件和操作系统的内存访问差异, 以实现让 Java 程序在各种平台下都能达到一致的内存访问效果.
定义 Java 内存模型并非一件容易的事情, 这个模型必须定义得足够严谨, 才能让 Java 的并发内存访问操作不会产生歧义; 但是, 也必须足够得宽松, 使得虚拟机的实现有足够的自由空间去利用硬件的各种特性 (寄存器, 高速缓存和指令集) 来获取更好的执行速度.
在 JMM 抽象模型中, 分为主内存, 工作内存. 主内存是所有线程共享的, 工作内存是每个线程独有的. 线程对变量的所有操作 (读取, 赋值) 都必须在工作内存中进行, 不能直接读写主内存中的变量. 并且不同的线程之间无法访问对方工作内存中的变量, 线程间的变量值的传递都需要通过主内存来完成, 他们三者的交互关系如下:
关于主内存和工作内存之间具体的交互协议, 即一个变量如何从主内存拷贝到工作内存, 如何从工作内存同步回主内存之类的实现细节, Java 内存模型定义上述 8 种操作来完成, 虚拟机实现时必须保证上面提及的每一种操作都是原子的, 不可再分的.
线程的安全性问题
原子性: 是指一个操作是不可中断的, 即使是在多线程环境下, 一个操作一旦开始就不会被其他线程影响.
可见性: 是指当一个线程修改了某个共享变量的值, 其他线程是否能够马上得知这个修改的值.
有序性: 是指程序执行的顺序按照代码的先后顺序执行.
在介绍有序性时, 有必要了解一下指令重排序. 在执行程序时, 为了提高性能, 编译器和处理器常常会对指令做重排序. 重排序分 3 种类型.
1)编译器优化的重排序. 编译器在不改变单线程程序语义的前提下, 可以重新安排语句的执行顺序.
2)指令级并行的重排序. 现代处理器采用了指令级并行技术 (Instruction-LevelParallelism,ILP) 来将多条指令重叠执行. 如果不存在数据依赖性, 处理器可以改变语句对应机器指令的执行顺序.
3)内存系统的重排序. 由于处理器使用缓存和读 / 写缓冲区, 这使得加载和存储操作看上去可能是在乱序执行.
来源: http://www.jianshu.com/p/7d3425a78d72