Java高并发(2) 并发基础

CPU多级缓存

CPU为何要有高速缓存

CPU的频率太快了,快到主存跟不上,这样在处理时钟周期内,CPU常常需要等待主存,浪费资源。
所以cache的出现,是为了缓解CPU和内存之间速度的不匹配问题(结构: cpu -> cache -> memory)

CPU在摩尔定律的指导下以每18个月翻一番的速度在发展,然而内存和硬盘的发展速度远远不及CPU。
这就造成了高性能能的内存和硬盘价格及其昂贵。然而CPU的高度运算需要高速的数据。
为了解决这个问题,CPU厂商在CPU中内置了少量的高速缓存以解决I\O速度和CPU运算速度之间的不匹配问题。

cpu-cache

CPU cache 意义

  1. 时间局部性:如果一个信息项正在被访问,那么在近期它很可能还会被再次访问。
  2. 空间局部性: 如果一个存储器的位置被引用,那么将来他附近的位置也会被引用。

带有高速缓存的CPU执行计算的流程

  1. 程序以及数据被加载到主内存
  2. 指令和数据被加载到CPU的高速缓存
  3. CPU执行指令,把结果写到高速缓存
  4. 高速缓存中的数据写回主内存

目前流行多级缓存结构

缓存一致性(MESI)

缓存行(Cache line): 缓存存储数据的单元

MESI 是指4中状态的首字母。每个Cache line有4个状态,可用2个bit表示,它们分别是:

状态 描述 监听任务
M 修改 (Modified) 该Cache line有效,数据被修改了,和内存中的数据不一致,数据只存在于本Cache中。 缓存行必须时刻监听所有试图读该缓存行相对就主存的操作,这种操作必须在缓存将该缓存行写回主存并将状态变成S(共享)状态之前被延迟执行。
E 独享、互斥 (Exclusive) 该Cache line有效,数据和内存中的数据一致,数据只存在于本Cache中。 缓存行也必须监听其它缓存读主存中该缓存行的操作,一旦有这种操作,该缓存行需要变成S(共享)状态。
S 共享 (Shared) 该Cache line有效,数据和内存中的数据一致,数据存在于很多Cache中。 缓存行也必须监听其它缓存使该缓存行无效或者独享该缓存行的请求,并将该缓存行变成无效(Invalid)。
I 无效 (Invalid) 该Cache line无效。

MESI状态转换:

cpu-cache

触发事件:

触发事件 描述
本地读取(Local read) 本地cache读取本地cache数据
本地写入(Local write) 本地cache写入本地cache数据
远端读取(Remote read) 其他cache读取本地cache数据
远端写入(Remote write) 其他cache写入本地cache数据

右边图示意了,当一个cache line的调整的状态的时候,另外一个cache line 需要调整的状态

举个栗子来说:
假设cache 1 中有一个变量x = 0的cache line 处于S状态(共享)。
那么其他拥有x变量的cache 2、cache 3等x的cache line调整为S状态(共享)或者调整为 I 状态(无效)。

指令重排和内存屏障

指令重排

现代CPU的速度越来越快,为了充分的利用CPU,在编译器和CPU执行期,都可能对指令重排。

cpu-out-of-order

在并发模型下,重排序还是可能会引发问题,比较经典的就是“单例模式失效”问题

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public class Singleton {
private static Singleton instance = null;

private Singleton() { }

public static Singleton getInstance() {
if(instance == null) {
synchronzied(Singleton.class) {
if(instance == null) {
instance = new Singleton(); //
}
}
}
return instance;
}
}

上面这段代码,初看没问题,但是在并发模型下,可能会出错,那是因为instance= new Singleton()并非一个原子操作,它实际上下面这三个操作:

1
2
3
memory = allocate();    //1:分配对象的内存空间
ctorInstance(memory); //2:初始化对象
instance = memory; //3:设置instance指向刚分配的内存地址

上面操作2依赖于操作1,但是操作3并不依赖于操作2,所以JVM是可以针对它们进行指令的优化重排序的,经过重排序后如下:

1
2
3
memory = allocate();    //1:分配对象的内存空间
instance = memory; //3:instance指向刚分配的内存地址,此时对象还未初始化
ctorInstance(memory); //2:初始化对象

可以看到指令重排之后,instance指向分配好的内存放在了前面,而这段内存的初始化被排在了后面。
在多线程场景下,可能A线程执行到了3,B线程发现已经不为空就返回继续执行,就会出错。

在java里面volatile可以防止重排,还有另外一个作用即内存可见性

内存屏障

硬件层的内存屏障分为两种:Load Barrier 和 Store Barrier即读屏障和写屏障。内存屏障有两个作用:

  1. 阻止屏障两侧的指令重排序;
  2. 强制把写缓冲区/高速缓存中的脏数据等写回主内存,让缓存中相应的数据失效。

对于volatile关键字,按照规范会有下面的操作:

  1. 在每个volatile写入之前,插入一个StoreStore,写入之后,插入一个StoreLoad
  2. 在每个volatile读取之前,插入LoadLoad,之后插入LoadStore

Java内存模型

jvm

Powered by Hexo and Hexo-theme-hiker

Copyright © 2013 - 2021 朝着牛逼的道路一路狂奔 All Rights Reserved.

访客数 : | 访问量 :