David John Wheeler有一句名言“计算机科学中的任何问题都可以通过加上一层间接层来解决”,一层不够就再加一层。后半句是我加的 (* ̄︶ ̄) ,虽然有点玩笑的意思,但是也的确能说明一些问题。计算机科学的确是靠着一层又一层的抽象与封装解决了巨量的问题。
我们来简单回顾一下:
……这样一层一层抽象包装下来,我们要想实现一个功能比如定时写文件等已经变成了很简单的事,只需要几行代码就搞定了。
public class FalseSharing {
private static AtomicLong time = new AtomicLong(0);
public static void main(String... args) throws InterruptedException {
int testNum = 50;
for (int i = 0 ; i< testNum;i++){// 测试50次
Thread thread = new Thread(new Job());
thread.start();
thread.join();
}
System.out.println(time.get()/1000/testNum + " us,avg");
}
static class Job implements Runnable{
@Override
public void run() {
int number = 8;
int iterationNumber = 20000;
CountDownLatch countDownLatch = new CountDownLatch(number);
Obj[] objArray = new Obj[number];
for (int i = 0;i < number;i++) {
objArray[i] = new Obj();
}
long start = System.nanoTime();
for (int i = 0;i < number;i++){
int ii = i;
Thread thread = new Thread(new Runnable() {
int iterationNumberInner = iterationNumber;
@Override
public void run() {
while (iterationNumberInner-->0){
objArray[ii].aLong+=1L;
}
countDownLatch.countDown();
}
});
thread.start();
}
try {
countDownLatch.await();
} catch (InterruptedException e) {
e.printStackTrace();
}
long end = System.nanoTime();
time.getAndAdd(end-start);
}
}
@Contended
private static final class Obj{
private volatile long aLong = 8L;//8Bytes
// private volatile long a=2L,b=2L,c=2L,d=2L,e=2L,f=2L,g=2L;//*****
}
}
全部代码在此,为了避免Java JIT(这也是一层抽象)的影响,我们每次执行都要加参数-Xint
来强制使用解释模式。
在我的机器上(4core,8processor,Core-i7),直接运行这段代码得到结果1是4594 us,avg这个级别,在结果1基础上把//*****
一行取消注释得到结果2是3916 us,avg 这个级别,在结果1基础上运行参数加上-XX:-RestrictContended
使得@Contended
起作用就能得到结果33466 us,avg。
这时候顶层用户就会莫名奇妙了,怎么多了几个字段运行时间反而减小了?怎么加上@Contended
后时间就更短了?
从Java代码这一层次的抽象来看,完全是没有问题的,那么问题究竟在哪呢?
我们知道一个CPU中的每个核是有自己的Cache的,高级别的L1是自己私有的,更低级别的L2、L3等可能是私有的,也可能是不同核共享的。这些不同级别的缓存(一次访问时间在几个ns左右)是用来弥补CPU的快速(一个周期通常零点几个ns)和内存访问的慢速(一次访问时间在几十个ns)之间的鸿沟的,而且是以CacheLine Size: N Bytes(Core-i7是64)为基本单位的,依据局部性原理一次性把内存中该访问变量周围的N Bytes内容拷贝到Cache中,如果一个对象不够N Bytes,就有可能和几个对象共用一个CacheLine,这样一个线程刷新Cacheline就会导致其他线程的缓存失效,要去更低级别的Cache甚至内存访问,就大大降低了访问速度。
这样回到刚才的问题,多加几个字段能在一定程度上增大该对象所占空间,减小共用CacheLine的几率,所以访问时间减少了,而@Contended
则使得一个对象一个CacheLine,直接帮我们避免了伪共享,所以访问时间更少了。要解决这个问题,光知道Java这一层抽象(语法、JDK API等)是不可能的,还得懂操作系统、甚至CPU芯片原理这些层抽象才行。
再比如说,JVM帮我们把C/C++的手动内存管理封装了一层抽象做到内存自动管理从而解放了我们,我们当然用得很爽,但是如果我们不懂这一层的抽象与封装,那么程序OOM的时候就只能傻眼了。
最后总结一下,计算机科学中的任何问题都可以通过加上一层间接层来解决,这是很正确的,但是也正是因为一层一层的抽象和包装,导致出了问题后很难定位,你都不知道问题究竟是出现在哪一层。所以要想提高技术水平不仅要知其然(看得见最顶层的包装)也要知其所以然(看得见底层的包装),每一层如果都懂或者说了解一些,那么出了问题很大程度上都可以凭直觉定位,即使不能凭直觉也可以通过各种手段debug,只会最顶层的抽象很多时候就只能望bug兴叹了。
访问原文,来自MageekChiu。
原创文章,作者:ItWorker,如若转载,请注明出处:https://blog.ytso.com/66233.html