JMM课程小结(摘抄整理)

JMM

硬件层数据一致性

intel 用MESI

https://www.cnblogs.com/z00377750/p/9180644.html

现代CPU的数据一致性实现 = 缓存锁 + 总线锁【早期的唯一实现】

 

读取缓存以cache line为基本单位,目前64bytes

位于同一缓存行的两个不同数据,被两个不同CPU锁定,产生互相影响的伪共享问题

伪共享问题:可以通过补位对齐解决(原理同上,缓存行64位,所以数据的前后只要留有占位的变量就可以避免和其他数据同个缓存行)

package top.gabin.jmm;

/**
 * CPU,硬件级别的缓存是一行读取的,一般是64位
 * <p>
 * 如果4字节的x和4字节的y同时存储在同一个缓存行中
 * <p>
 * 那么当对x做写操作的时候,需要阻塞对y的操作
 * 对y的操作也要阻塞x的操作
 * <p>
 * 所以理论上,如果能够将锁需要读取的数据独立在一个缓存行中,或者在同一个缓存行的其他数据只读,则可以提高读写效率
 */
public class CPUCacheLinePadding {

    static class DataContainer {
        private volatile long p1, p2, p3, p4, p5, p6, p7;
        public volatile long data; // 真正有用的数据
        private volatile long p8, p9, p10, p11, p12, p13, p14;

    }

    static class DataContainer1 {
        public volatile long data;

    }

    public static void main(String[] args) throws InterruptedException {
        // 伪缓存问题

        // 1、使用了补齐的方式,使得真正有意义的数据被保存在一个缓存行中,不和其他会被写的数据一起
        long start = System.currentTimeMillis();
        test1();
        System.out.println("补齐,解决伪共享问题:" + (System.currentTimeMillis() - start));
        // 2、普通的方式
        start = System.currentTimeMillis();
        test2();
        System.out.println("一般场景:" + (System.currentTimeMillis() - start));

    }

    private static void test1() throws InterruptedException {
        DataContainer[] containers = new DataContainer[2];
        containers[0] = new DataContainer();
        containers[1] = new DataContainer();

        Thread t1 = new Thread(() -> {
            for (int j = 0; j < 100_000_000; j++) {
                containers[0].data = j;
            }
        });
        t1.start();
        Thread t2 = new Thread(() -> {
            for (int j = 0; j < 100_000_000; j++) {
                containers[1].data = j;
            }
        });
        t2.start();
        t1.join();
        t2.join();

    }

    private static void test2() throws InterruptedException {
        DataContainer1[] containers = new DataContainer1[2];
        containers[0] = new DataContainer1();
        containers[1] = new DataContainer1();
        Thread t1 = new Thread(() -> {
            for (int j = 0; j < 100_000_000; j++) {
                containers[0].data = j;
            }
        });
        t1.start();
        Thread t2 = new Thread(() -> {
            for (int j = 0; j < 100_000_000; j++) {
                containers[1].data = j;
            }
        });
        t2.start();
        t1.join();
        t2.join();
    }

}

 

乱序问题

一、CPU为了提高指令执行效率,会在一条指令执行过程中(比如去内存读数据(慢100倍)),去同时执行另一条指令,前提是,两条指令没有依赖关系

https://www.cnblogs.com/liushaodong/p/4777308.html

package top.gabin.jmm;

/**
 * CPU重排序的问题
 * <p>
 * 如果指令1和指令2之间没有依赖关系,那么在CPU执行的时候,可能先执行指令2,再去执行指令1
 * <p>
 * 比如说一般一个方法的JVM汇编指令第一个是去加载this对象,从内存中读取的速度远低于CPU 寄存器执行的时间,这时候可能
 * 第二条指令只需要执行1ms,那么有可能,CPU会选择先执行第二条指令
 * <p>
 * 这个在多线程中可能存在一个执行顺序的问题,
 * 比如
 * <p>
 * 线程1
 * b=1
 * x=a
 * <p>
 * 线程2
 * a=1
 * y=b
 * <p>
 * 看上去线程1的两行代码和线程2的两行代码没有直接关联,所以存在CPU指令乱序的可能性,
 */
public class DisOrder {

    private static int x, y, a, b;

    public static void main(String[] args) throws InterruptedException {
        for (; ; ) {
            x = 0;
            y = 0;
            a = 0;
            b = 0;
            Thread t1 = new Thread(() -> {
                // 不加这行的话,如果CPU执行速度很快,说不准,压根就测不出来,这个问题可以类比到某些手机端IOS根本出现不了问题,但是ANDROID就会
                // 以前大概遇到过一次是IOS渲染比较快,所以就不会出现问题,ANDROID渲染执行比较慢,出现了执行顺序的问题
                shortWait(100000);
                // 这里必须是很单纯的代码调用,不能是非原子性的,比如long,64位,不是原子性操作
                a = 1;
                x = b;
            });
            Thread t2 = new Thread(() -> {
                // 这里必须是很单纯的代码调用,不能是非原子性的,比如long,64位,不是原子性操作
                b = 1;
                y = a;
            });
            t1.start();
            t2.start();
            t1.join();
            t2.join();
            if (x == 0 && y == 0) {
                System.out.println("存在CPU指令重排,x==y==0");
                break;
            }
        }
    }

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

}

 

 

二、写操作也可以进行合并

https://www.cnblogs.com/liushaodong/p/4777308.html

原理:CPU中存在一个4槽缓存 :WriteCombineBuffer,比L1级别的缓存访问速度更快。满4位可以触发一次合并写,下面是一个证明合并写更快的例子

package top.gabin.jmm;

/**
 * 存在一个WCBuffer的缓存,四个槽
 *
 * 这个缓存比CPU的L1缓存更快
 *
 * 如果能触发合并写,写的速度将会更快
 */
public class WriteCombine {

    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(String[] args) {
        for (int i = 0; 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;
    }

}

 

如何保证特定情况下不乱序

硬件内存屏障 X86

sfence: store| 在sfence指令前的写操作当必须在sfence指令后的写操作前完成。

lfence:load | 在lfence指令前的读操作当必须在lfence指令后的读操作前完成。

mfence:modify/mix | 在mfence指令前的读写操作当必须在mfence指令后的读写操作前完成。

原子指令,如x86上的”lock …” 指令是一个Full Barrier,执行时会锁住内存子系统来确保执行顺序,甚至跨多个CPU。Software Locks通常使用了内存屏障或原子指令来实现变量可见性和保持程序顺序

JVM级别如何规范(JSR133)【8大原子性操作描述已废弃,但实现未修改】

LoadLoad屏障: 对于这样的语句

Load1;

LoadLoad;

Load2,

在Load2及后续读取操作要读取的数据被访问前,保证Load1要读取的数据被读取完毕。

 

StoreStore屏障:

对于这样的语句

Store1;

StoreStore;

Store2,

在Store2及后续写入操作执行前,保证Store1的写入操作对其它处理器可见。

 

LoadStore屏障:

对于这样的语句

Load1;

LoadStore;

Store2,

在Store2及后续写入操作被刷出前,保证Load1要读取的数据被读取完毕。

 

StoreLoad屏障:

对于这样的语句

Store1;

StoreLoad;

Load2,

​ 在Load2及后续所有读取操作执行前,保证Store1的写入对所有处理器可见。

volatile的实现细节

  1. 字节码层面 ACC_VOLATILE

  2. JVM层面 volatile内存区的读写 都加屏障

    StoreStoreBarrier

    volatile 写操作

    StoreLoadBarrier

    LoadLoadBarrier

    volatile 读操作

    LoadStoreBarrier

  3. OS和硬件层面 https://blog.csdn.net/qq_26222859/article/details/52235930 hsdis - HotSpot Dis Assembler windows lock 指令实现 | MESI实现

synchronized实现细节

  1. 字节码层面 ACC_SYNCHRONIZED monitorenter monitorexit
  2. JVM层面 C C++ 调用了操作系统提供的同步机制
  3. OS和硬件层面 X86 : lock cmpxchg / xxx https://blog.csdn.net/21aspnet/article/details/88571740
posted @ 2020-09-01 21:09  gabin  阅读(240)  评论(0编辑  收藏  举报