首页 并发基础 - 术语扫盲
文章
取消

并发基础 - 术语扫盲

专业术语

1.1 高速缓存

现在计算机中,由于CPU的频率远高于内存的频率,所以在cpu操作内存数据的时候,如果都需要等待去主内存中读取数据的话, 那cpu的执行速度将会相当慢。于是为了加快速度,现代CPU都会配备有L1, L2, L3级别的缓存。每一级的增加对应频率的降低和缓存大小的增加。

1.2 缓存行

高速缓存的最小单位是以“缓存行”为单位进行读取和修改。通常一个缓存行的大小是64byte。CPU读取修改高速缓存,还有高速缓存和内存做交换都是以缓存行作为单位。

高速缓存加载策略的理论基础:

  • 时间局部性:最近被cpu访问的数据,短期还要继续访问
  • 空间局部性:cpu访问数据附近的数据,短期还要继续访问

因此线程在操作一个内存对象的时候, 会将这个对象在内存中布局的数据都加载进去,同时还会加载内存布局中相邻的对象(如果同一个缓存行里面还能装)

1.3 伪共享

每个cpu核心都有自己独享的L1 L2高速缓存

高速缓存读时机:cpu需要操作内存中的东西的时候高速缓存就把数据从主内存中缓存下来,以“缓存行”为单位进行保存在高速缓存中,高速缓存写时机:当这个缓存行存放的数据需要被替换的时候(有更新的热点数据需要缓存)1

由于每个cpu核心都会有自己的一份高速缓存,有可能多个核心的高速缓存了同一个内存地址的数据,因为有可能多个线程操作了同一个类。那么CPU如何保证每个核心的高速缓存保持一致不产生脏数据呢?

  1. 总线嗅探机制:高速缓存间的广播机制,把写请求广播到各个核心上,核心根据自己的高速缓存情况进行处理自己的高速缓存
  2. MESI协议2:上面有了这种广播机制以后如何保证广播的时序呢,就通过这个MESI协议,本质是一个写失效协议,某个核心更新“缓存行”的数据的时候通过嗅探机制和这个协议告诉别的核心,你的缓存行失效了。

由于缓存行的单位是64byte,加入有两个变量x和y,这两个变量的对象都缓存在了一个缓存行中,cpu核心1需要修改x,cpu核心2需要修改y,于是他们需要频繁地争夺这个缓存行的写权限。这里面就存在了“数据竞争”,或者理解为锁。但是在应用层的程序代码上看的话,我的两个线程操作的是两个对象变量,两个线程的操作是独立的,不应该会存在冲突或者竞争问题,但是在CPU层面的确存在了竞争的情况,这就是伪共享

1.4 内存屏障

上面说的嗅探和MESI,只是把处理器的实现抽象了一个共性出来,但事实是在真实的CPU内存模型实现中,cpu的缓存模型都是不尽相同的,有的cpu内存模型设计是强一致性内存模型,各个核心能看到别的核心写入的数据,能让各个核心的在自己的缓存上的值都保持一致。但是有的CPU的内存模型是弱一致性模型,就是我能保证最后各个核心的数据能保持一致,但中间的时间差我不保证多久。这种处理器中想让别的核心能实时看见的话自己的数据的话就需要一个主动同步缓存的方式,这种方式就叫“内存屏障”;

高级语言中一般就通过锁的方式进行提供内存屏障的支持。调用lock, unlock就是内存屏障高级语言实现。java中volatile也是

1.5 字节填充

上面1.3说的一个变量或者数据进入到CPU高速缓存的时候是以缓存行存在的,通常缓存行是64byte。为了不发生伪共享的问题, 我们把一个数据结构的变量对象在内存中将它补充到64byte,保证一个缓存行里面操作的就是一个变量,而不会发生伪共享的问题。这个补充的动作就叫做字节填充,通常是定义一下无用的数据类型,如long进行填充。

1.6 重排序

简单点来说,就是代码中展示的对于单线程的执行语义来说是顺序执行的,像如下的代码:

1
2
3
4
5
6
int a,b;
public void seemToBeOrder(){
	a = 1;
  b = 2;
  //略...
}

这段代码按照方法是从上往下顺序执行的语义来理解, 如果用另一个线程去打印变量b且能看到b=2, 那么a肯定是等于1的。但实际上这并不是一定的。因为在实际的执行过程中,这些都是一条条的计算机指令,这些指令在执行过程中可能会发生乱序,可能发生乱序的地方有:

  1. jvm的指令重排序,编译器,JIT即时编译器
  2. cpu核心最终执行指令的时候,只能保证有控制依赖, 数据依赖,地址依赖等等依赖关系的指令有先后顺序。

1.7 同步

在高级语言的开发中我们所说的同步包含了两层意思, 一个是数据的同步, 另一个是动作执行的顺序。上面说的MESI协议保证的是多核cpu高速缓存的数据一致性。但是它并不能保证一串动作执行的顺序,那么我们来看一下动作的顺序在什么地方可能会乱掉:

  1. 指令重排序,能让单线程执行顺序打乱
  2. 多核读写顺序,多核读写的

1.8 java并发关键字

java中实现同步的关键字由volatile和synchronized组成。这两个关键字底层是如何实现同步的呢?

  1. 对于数据的同步,编译后的实现会用到内存屏障指令,让每个线程进入到同步块里面的时候看到的数据都是上一个线程操作完的数据
  2. 对于执行顺序的保证,禁止指令重排序(jvm层面禁止,CPU层面将多个指令封装成一个原子性指令,不允许CPU乱序执行这部分指令)

volatile与使用synchronized相比,声明一个volatile字段的区别在于没有涉及到锁操作。但特别的是对volatile字段进行“++”这样的读写操作不会被当做原子操作执行。

附录

如何查看缓存行大小

linux可以通过下面的命令查看cpu缓存的缓存行大小

cat /sys/devices/system/cpu/cpu0/cache/index0/coherency_line_size

osx可以通过sysctl -a命令进行查看

sysctl -a | grep hw

hw.cachelinesize: 64

hw.l1icachesize: 32768

hw.l1dcachesize: 32768

hw.l2cachesize: 262144

hw.l3cachesize: 9437184

参考

并发编程目录

CPU Cache与缓存行

指令重排序

Java内存访问重排序的研究

从Java视角理解系统结构(二)CPU缓存

[^1]既然CPU有缓存一致性协议(MESI),为什么JMM还需要volatile关键字?

本文由作者按照 CC BY 4.0 进行授权

sql基础

Maven入门