今天查了很多资料,主要是想搞清楚写JAVA和CacheLine有什么关系以及我们如何针对CacheLine写出更好的JAVA程序。
CPU和内存
CPU是计算机的大脑,它负责运算,内存是数据,它为CPU提供数据。这里之所以忽略其他存储设备是为了简化模型。假设我们面对的是具有两个核心的CPU,那么我们的模型大概如下面的样子:
CPU计算核心不会直接和内存打交道,它会直接从缓存拿数据,如果缓存没拿到,专业点说即缓存未命中的时候才会去内存去拿,同时会更新缓存。这个过程CPU不会仅仅读取需要的某个字节或字的内容,而会按策略读取一块内容。典型的处理器策略是向前进的方向读取小于2048字节的数据。如上图所示,L1缓存(一级缓存)离CPU内核最近,容量也最小同时造价也最高,属于内核独立使用。L2缓存离得远些,容量比L1大写但是还是属于独立的内核使用。L3缓存离的最远,也是最慢的缓存,这层缓存为所有内核共享。
缓存行
上面一节我们介绍了CPU和内存之间的模型,本节介绍下缓存行。CPU从缓存中读取内容并不是一个字节或一个字读的,而是一行一行,也可以理解为一块一块读的。CPU是这样设计的,我们想想,相邻数据的相关性往往很大,这么设计可以提高缓存的命中率,也降低访问缓存的次数。上面提到的一行叫做缓存行。典型的大小是32-256字节,其中最常见的是64字节。这是缓存一致性的最小粒度,如果一行中有一个字节哪怕一个位的内容内修改并写回内存,那么其他内核的该缓存行将会被标志位无效。假设两个内核正在执行不同的线程,并且操作同一个缓存行,A线程修改了缓存行的第一个字节,B线程需要访问第二个字节,这个时候该缓存行其实已经被进行了一次和内存同步的操作,保证该段和内存中该行数据一致,然而这个过程B线程访问的这个字节和第一个线程访问的字节并没有关系。这个时候有同学有疑问了,那么如果两个内核的线程同时对该段进行操作,也就是没有谁先谁后的情况,会出现什么情况呢。其实这里涉及到另外一个概念,叫缓存一致性协议(Cache Coherency)。即我们已经有了一个保证:在任意时刻,任意级别的缓存段中的内容,等同于它对应的内存中的内容。
关于AtomicReference
AtomicReference是由JAVA5引入的,用于对一个对象引用进行原子操作,我们可以看到AtomicReference的实现是用CAS技术对引用进行指令级别的原子修改然后再利用volatile带来的内存屏障特性保证引用的修改对其他线程立即可见。这里提一点,由volatile修饰的变量在写之后会插入一个store屏障,在读之前插入一个load屏障。store屏障保证写操作被后面的线程立即可见。load屏障保证所有的读操作之前的写立即生效。然而AtomicReference并没有避免缓存行带来的缓存命中率问题。一个AtomicReference对象包括一个volatile的对象引用,即这个对象在32位操作系统中占4个字节,在64位操作系统中占8个字节。虽然多个线程对同一个AtomicReference对象操作没有并发问题,但是当多个线程对多个AtomicReference操作的时候就有可能有缓存命中率问题。借着上文中的模型我们假设两个AtomicReference变量A和B位于同一内存相邻区域,当在核心1执行的线程对A变量操作的时候CPU会将A变量读入核心1的缓存区域,同时捎带把B变量读入缓存区域,此时和A变量位于同一缓存行。核心2执行的另外一个线程同时对B进行操作,这个时候该缓存行已经失效,会发生一次读内存操作。
缓存行填充
Exchanger类是JAVA5提供的用于多线程之间交换数据的工具类,我们看看Exchanger的内部类Slot的实现:
1 2 3 4 |
|
Slot只是简单的继承了AtomicReference类,并声明了15个long类型的变量。如果不懂CacheLine的话不会明白这段无用变量的意义,这里声明了15个long类型的变量,一个long类型为8个字节,加上上面的引用在64位操作系统环境下为128字节,32位操作系统环境下的124字节也没问题,因为两个Slot类型变量不可能位于同一缓存行,这也就解决了多核CPU环境下的缓存行失效问题。