底层知识总结

超线程:一个ALU对应多个PC | Registers

所谓的四核八线程

ALU -> Arithmetic & Logic Unit 运算单元

 存储器的层次结构

为什么存在缓存:CPU的速度太快,内存速度太慢

越靠近 CPU 的缓存越快也越小。

所以 L1 缓存很小但很快,并且紧靠着在使用它的 CPU 内核。

L2 大一些,也慢一些,并且仍然只能被一个单独的 CPU 核使用。

L3 在现代多核机器中更普遍,仍然更大,更慢,并且被单个插槽上的所有 CPU 核共享。

最后,主存保存着程序运行的所有数据,它更大,更慢,由全部插槽上的所有 CPU 核共享。

当 CPU 执行运算的时候,它先去 L1 查找所需的数据,再去 L2,然后是 L3,最后如果这些缓存中都没有,所需的数据就要去主内存拿。

走得越远,运算耗费的时间就越长。

所以如果进行一些很频繁的运算,要确保数据在 L1 缓存中。

按块读取

读取数据时,由于局部性原理,将相邻的数据一块读取。可以提高效率。(不管是硬盘往内存读或者内存往缓存读)

缓存行Cache Line:

每个缓存里面都是由缓存行组成的,缓存系统中是以缓存行(cache line)为单位存储的。缓存行是2的整数幂个连续字节,一般为32-256个字节。

缓存行越大,局部性空间效率越高,但读取时间慢

缓存行越小,局部性空间效率越低,但读取时间快

取一个折中值,最常见的缓存行大小是64个字节

多个CPU时,需要保持数据的一致性,这里用的是一致性协议

MESI Cache一致性协议

伪共享:

当多线程修改互相独立的变量时,如果这些变量共享同一个缓存行,就会无意中影响彼此的性能,这就是伪共享。

在程序运行的过程中,缓存每次更新都从主内存中加载连续的 64 个字节。因此,如果访问一个 long 类型的数组时,当数组中的一个值被加载到缓存中时,另外 7 个元素也会被加载到缓存中。

但是,如果使用的数据结构中的项在内存中不是彼此相邻的,比如链表,那么将得不到免费缓存加载带来的好处。

不过,这种免费加载也有一个坏处。设想如果我们有个 long 类型的变量 a,它不是数组的一部分,而是一个单独的变量,并且还有另外一个 long 类型的变量 b 紧挨着它,那么当加载 a 的时候将免费加载 b。

看起来似乎没有什么毛病,但是如果一个 CPU 核心的线程在对 a 进行修改,另一个 CPU 核心的线程却在对 b 进行读取。

当前者修改 a 时,会把 a 和 b 同时加载到前者核心的缓存行中,更新完 a 后其它所有包含 a 的缓存行都将失效,因为其它缓存中的 a 不是最新值了。

而当后者读取 b 时,发现这个缓存行已经失效了,需要从主内存中重新加载。

请记住,我们的缓存都是以缓存行作为一个单位来处理的,所以失效 a 的缓存的同时,也会把 b 失效,反之亦然。

伪共享例子:

package c_020;

public class FalseSharing {
    public static void main(String[] args) throws InterruptedException {
        testPointer(new Pointer());
    }

    private static void testPointer(Pointer pointer) throws InterruptedException {
        long start = System.currentTimeMillis();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 100000000; i++) {
                pointer.x++;
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 100000000; i++) {
                pointer.y++;
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println(System.currentTimeMillis() - start);
        System.out.println(pointer);
    }
}
class Pointer {
    volatile long x;
    volatile long y;
}

这个例子中,我们声明了一个 Pointer 的类,它包含 x 和 y 两个变量(必须声明为volatile,保证可见性,关于内存屏障的东西我们后面再讲),一个线程对 x 进行自增1亿次,一个线程对 y 进行自增1亿次。

可以看到,x 和 y 完全没有任何关系,但是更新 x 的时候会把其它包含 x 的缓存行失效,同时也就失效了 y,运行这段程序输出的时间为3258ms

避免伪共享

伪共享的原理我们知道了,一个缓存行是 64 个字节,一个 long 类型是 8 个字节,所以避免伪共享也很简单,大概有以下三种方式:

(1)在两个 long 类型的变量之间再加 7 个 long 类型

package c_020;

public class FalseSharing2 {
    public static void main(String[] args) throws InterruptedException {
        testPointer(new Pointer2());
    }

    private static void testPointer(Pointer2 pointer) throws InterruptedException {
        long start = System.currentTimeMillis();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 100000000; i++) {
                pointer.x++;
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 100000000; i++) {
                pointer.y++;
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println(System.currentTimeMillis() - start);
        System.out.println(pointer);
    }
}
class Pointer2 {
    volatile long x;
    long p1, p2, p3, p4, p5, p6, p7;
    volatile long y;
}

再次运行程序,会发现输出时间神奇的缩短为了499ms

(2)重新创建自己的 long 类型,而不是 java 自带的 long

class Pointer {
    MyLong x = new MyLong();
    MyLong y = new MyLong();
}

class MyLong {
    volatile long value;
    long p1, p2, p3, p4, p5, p6, p7;
}

这种个人感觉和第一种一样,就不演示了

(3)使用 @sun.misc.Contended 注解(java8)

修改 MyLong 如下:

@sun.misc.Contended
class MyLong {
    volatile long value;
}
package c_020;

public class FalseSharing3 {
    public static void main(String[] args) throws InterruptedException {
        testPointer(new Point3());
    }

    private static void testPointer(Point3 pointer) throws InterruptedException {
        long start = System.currentTimeMillis();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 100000000; i++) {
                pointer.x.value++;
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 100000000; i++) {
                pointer.y.value++;
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println(System.currentTimeMillis() - start);
        System.out.println(pointer);
    }
}
class Point3{
    MyLong x = new MyLong();
    MyLong y = new MyLong();
}
@sun.misc.Contended
class MyLong {
    volatile long value;
}

或者:

package c_020;

import sun.misc.Contended;

public class FalseSharing {
    public static void main(String[] args) throws InterruptedException {
        testPointer(new Pointer());
    }

    private static void testPointer(Pointer pointer) throws InterruptedException {
        long start = System.currentTimeMillis();
        Thread t1 = new Thread(() -> {
            for (int i = 0; i < 100000000; i++) {
                pointer.x++;
            }
        });

        Thread t2 = new Thread(() -> {
            for (int i = 0; i < 100000000; i++) {
                pointer.y++;
            }
        });

        t1.start();
        t2.start();
        t1.join();
        t2.join();

        System.out.println(System.currentTimeMillis() - start);
        System.out.println(pointer);
    }
}
class Pointer {
    @Contended
    volatile long x;
    @Contended
    volatile long y;
}

默认使用这个注解是无效的,需要在JVM启动参数加上-XX:-RestrictContended才会生效,再次运行程序发现时间是634ms

注意,以上三种方式中的前两种是通过加字段的形式实现的,加的字段又没有地方使用,可能会被jvm优化掉,所以建议使用第三种方式。

缓存行对齐:

对于有些特别敏感的数字,会存在线程高竞争的访问,为了保证不发生伪共享,可以使用缓存航对齐的编程方式,上面避免非共享的方式,就是缓存行对齐。

CPU的乱序执行

乱序证明:如果没有乱序,那么绝对不会出现x=0,y=0的情况

public class T04_Disorder {
    private static int x = 0, y = 0;
    private static int a = 0, b =0;

    public static void main(String[] args) throws InterruptedException {
        int i = 0;
        for(;;) {
            i++;
            x = 0; y = 0;
            a = 0; b = 0;
            Thread one = new Thread(new Runnable() {
                public void run() {
                    //由于线程one先启动,下面这句话让它等一等线程two. 读着可根据自己电脑的实际性能适当调整等待时间.
                    //shortWait(100000);
                    a = 1;
                    x = b;
                }
            });

            Thread other = new Thread(new Runnable() {
                public void run() {
                    b = 1;
                    y = a;
                }
            });
            one.start();other.start();
            one.join();other.join();
            String result = "第" + i + "次 (" + x + "," + y + ")";
            if(x == 0 && y == 0) {
                System.err.println(result);
                break;
            } else {
                //System.out.println(result);
            }
        }
    }


    public static void shortWait(long interval){
        long start = System.nanoTime();
        long end;
        do{
            end = System.nanoTime();
        }while(start + interval >= end);
    }
}

禁止指令重排序:

CPU层面如何禁止重排序

使用:内存屏障

原理:对某部分内存做操作时前后添加的屏障,屏障前后的操作不可以乱序执行

Intel中的屏障为:

lfence  mfence sfence原语,当然也可以使用总线锁来解决

sfence(Store Barrier):在sfence指令前的写操作必须在sfence指令后的写操作前完成。 
lfence(Load Barrier):在lfence指令前的读操作必须在lfence指令后的读操作前完成。 
mfence(是一种全能型的屏障,具备ifence和sfence的能力):在mfence指令前的读写操作必须在mfence指令后的读写操作前完成

Lock前缀,Lock不是一种内存屏障,但是它能完成类似内存屏障的功能。Lock会对CPU总线和高速缓存加锁,可以理解为CPU指令级的一种锁。它后面可以跟ADD, ADC, AND, BTC, BTR, BTS, CMPXCHG, CMPXCH8B, DEC, INC, NEG, NOT, OR, SBB, SUB, XOR, XADD, and XCHG等指令。

Lock前缀实现了类似的能力:

1. 它先对总线/缓存加锁,然后执行后面的指令,最后释放锁后会把高速缓存中的脏数据全部刷新回主内存。

2. 在Lock锁住总线的时候,其他CPU的读写请求都会被阻塞,直到锁释放。Lock后的写操作会让其他CPU相关的cache line失效,从而从新从内存加载最新的数据。这个是通过缓存一致性协议做的。

 

JSR内存屏障(JVM层级的)

LoadLoad屏障:

        对于这样的语句Load1;LoadLoad;Load2,在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。

StoreStore屏障:

        对于这样的语句Strore1;StoreStore;Store2,在Store2及后续写入操作执行前,保证Store1的写入操作对其他处理器可见。

LoadStore屏障:

        对于这样的语句Load1;LoadStore;Store2,在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。

StoreLoad屏障:

        对于这样的语句Store1;StoreLoad;Load2;,在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。

注意:volatile底层(CPU层面)使用的是lock指令

as if serial

不管如何重排序,单线程执行结果不会改变。

合并写:(不重要)

Write Combining Buffer

一般是4个字节

由于ALU速度太快,所以在写入L1的的同时,写入一个WC Buffer,满了之后,再直接更新到L2。

证明:

public final class WriteCombining {

    private static final int ITERATIONS = Integer.MAX_VALUE;
    private static final int ITEMS = 1 << 24;
    private static final int MASK = ITEMS - 1;

    private static final byte[] arrayA = new byte[ITEMS];
    private static final byte[] arrayB = new byte[ITEMS];
    private static final byte[] arrayC = new byte[ITEMS];
    private static final byte[] arrayD = new byte[ITEMS];
    private static final byte[] arrayE = new byte[ITEMS];
    private static final byte[] arrayF = new byte[ITEMS];

    public static void main(final String[] args) {

        for (int i = 1; i <= 3; i++) {
            System.out.println(i + " SingleLoop duration (ns) = " + runCaseOne());
            System.out.println(i + " SplitLoop  duration (ns) = " + runCaseTwo());
        }
    }

    public static long runCaseOne() {
        long start = System.nanoTime();
        int i = ITERATIONS;

        while (--i != 0) {
            int slot = i & MASK;
            byte b = (byte) i;
            arrayA[slot] = b;
            arrayB[slot] = b;
            arrayC[slot] = b;
            arrayD[slot] = b;
            arrayE[slot] = b;
            arrayF[slot] = b;
        }
        return System.nanoTime() - start;
    }

    public static long runCaseTwo() {
        long start = System.nanoTime();
        int i = ITERATIONS;
        while (--i != 0) {
            int slot = i & MASK;
            byte b = (byte) i;
            arrayA[slot] = b;
            arrayB[slot] = b;
            arrayC[slot] = b;
        }
        i = ITERATIONS;
        while (--i != 0) {
            int slot = i & MASK;
            byte b = (byte) i;
            arrayD[slot] = b;
            arrayE[slot] = b;
            arrayF[slot] = b;
        }
        return System.nanoTime() - start;
    }
}

NUMA---Non uniform Memory Access

先看UMA

ZGC-NUMA aware

 

 

 

 

 

 

 

 

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值