JVM第三课:复习、Linking、Java内存模型

前情回顾:类加载机制

在这里插入图片描述

双亲委派

parent只是一个成员变量,不是继承关系
在这里插入图片描述

上节课遗留问题

parent是怎么指定的?

手动指定parent:
在这里插入图片描述

双亲委派机制可以被打破吗?

双亲委派机制是在ClassLoader类里的LoadClass()方法已经写死的,你只需重写FingClass()方法就可以了。那怎么打破它呢?
在这里插入图片描述

热加载的实现原理

Tomcat把整个ClassLoader全部干掉,再用自己实现的ClassLoader把新修改过的类的Class重新Load一遍。

package com.mashibing.jvm.c2_classloader;

import com.mashibing.jvm.Hello;

import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;

public class T012_ClassReloading2 {
    private static class MyLoader extends ClassLoader {
        @Override
        public Class<?> loadClass(String name) throws ClassNotFoundException {

            File f = new File("C:/work/ijprojects/JVM/out/production/JVM/" + name.replace(".", "/").concat(".class"));

            if(!f.exists()) return super.loadClass(name);

            try {

                InputStream is = new FileInputStream(f);

                byte[] b = new byte[is.available()];
                is.read(b);
                return defineClass(name, b, 0, b.length);
            } catch (IOException e) {
                e.printStackTrace();
            }

            return super.loadClass(name);
        }
    }

    public static void main(String[] args) throws Exception {
        MyLoader m = new MyLoader();
        Class clazz = m.loadClass("com.mashibing.jvm.Hello");

        m = new MyLoader();
        Class clazzNew = m.loadClass("com.mashibing.jvm.Hello");

        System.out.println(clazz == clazzNew);
    }
}

Linking

在这里插入图片描述

证明默认值的存在

输出结果为2. 如果交换10 11行,输出结果为3

package character03;

public class T001_ClassLoadingProcedure {
    public static void main(String[] args) {
        System.out.println(T.count);
    }
}

class T {
    public static T t = new T(); // 执行到这一步时,下一行的count仍然是默认值0,调用构造方法后,count变为1,
    public static int count = 2; // 执行到这一步时,上一行的count=1被这一行的count=2覆盖
    private T() {
        count ++;
    }
}

New 一个对象的过程

  • 先分配内存
  • 再赋默认值
  • 再赋初始值

面试题:DCL(Double Check Lock)单例要不要加Volitile?
需要。因为有指令重排的问题。
初始化一半的时候,单例已经存在了,处于半初始化的状态。此时另外一个线程来了,直接把半初始化的对象取走了,出现值的问题。

package com.mashibing.jvm.c0_basic;

public class C {
    public static void main(String[] args) {
        Object o = new Object();
    }
}

下图4、7可能会发生重排
在这里插入图片描述
new: Create new object
dup: 复制操作数堆栈上的顶部值,并将复制的值压入操作数堆栈。
invoke special: 调用实例方法; 对超类、私有和实例初始化方法调用的特殊处理
astore _< n >: 将引用存储到局部变量中。操作数堆栈顶部的objectref的类型必须是returnAddress或reference类型。它从操作数堆栈中弹出,并将处的局部变量的值设置为objectref。

JMM(Java Memory Model)

硬件层的并发优化基础知识

在这里插入图片描述
这个金字塔,从最上层L0开始,越往下 速度越慢,成本越低,空间越大
L0 L1 L2是CPU内部的缓存,每个CPU或者CPU的核心独享

读取速度:CPU 远大于 内存 远大于 磁盘
这个远大于大概就是100倍+
在这里插入图片描述

产生数据的不一致问题:两个CPU之间的缓存怎么保持一致?

在这里插入图片描述
老的CPU,总线锁:效率偏低
在这里插入图片描述
新的CPU:缓存锁: 各种各样的一致性协议,例如,intel的MESI协议
4种状态的含义:我改过、只有我在用(独享)、我读别人也在读、失效
在这里插入图片描述
现代CPU的数据一致性实现=缓存锁(MESI…)+总线锁

缓存行

简单理解是我要读取一个数据,CPU不止装载这个数据,还会装载很多其他可能会用到的数据进来

读取缓存以cacheline为基本单位,多数的实现为64kB(64字节,512位)

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

示例1:两个线程修改同一个数组中的值。执行时间约250

package character03;

import java.util.Random;

public class T01_CacheLinePadding {
    private static class T {
        public volatile long x = 0L;
    }

    public static T[] arr = new T[2];

    static {
        arr[0] = new T();
        arr[1] = new T();
    }

    public static void main(String[] args) throws Exception {
        Thread t1 = new Thread(()->{
            for (long i = 0; i < 1000_0000L; i++) {
                arr[0].x = i;
            }
        });

        Thread t2 = new Thread(()->{
            for (long i = 0; i < 1000_0000L; i++) {
                arr[1].x = i;
            }
        });

        final long start = System.nanoTime();
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println((System.nanoTime() - start)/100_0000);
    }
}

示例2:使用7个long类型的数字(7个*8字节=56字节)将缓存行填起来,加上变量x本身占用的空间,使两个T对象恰巧不在统一缓存行。耗时80
通过缓存行对齐后,效率提升了。

package character03;

public class T02_CacheLinePadding {
    private static class Padding {
        public volatile long p1, p2, p3, p4, p5, p6, p7;
    }

    private static class T extends Padding {
        public volatile long x = 0L;
    }

    public static T[] arr = new T[2];

    static {
        arr[0] = new T();
        arr[1] = new T();
    }

    public static void main(String[] args) throws Exception {
        Thread t1 = new Thread(() -> {
            for (long i = 0; i < 1000_0000L; i++) {
                arr[0].x = i;
            }
        });

        Thread t2 = new Thread(() -> {
            for (long i = 0; i < 1000_0000L; i++) {
                arr[1].x = i;
            }
        });

        final long start = System.nanoTime();
        t1.start();
        t2.start();
        t1.join();
        t2.join();
        System.out.println((System.nanoTime() - start) / 100_0000);
    }
}

开源的disruptor就考虑到了缓存行问题。
在这里插入图片描述

CPU乱序执行

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

CPU乱序执行的根源,参考 cpu乱序执行
在这里插入图片描述

读的乱序执行

比如CPU要去读L3中的一个数据,这个读的速度对CPU来说很慢,它就判断如果下面的指令和这个数据没有直接关系,就可能同时去执行下面的指令.

生活中的例子就是下main这个泡茶的过程:
在这里插入图片描述

合并写(WC)

在这里插入图片描述

提到乱序时,一般也会提到合并写,尽管合并写其实并不算是乱序执行,这里就简单了解一下.

WCBuffers: WriteCombiningBuffers,这个WCBuffers的个数依赖cpu模型,目前测试是和CPU的核数相等.
也就是CPU有几颗核心,那么WCBuffers一次就可以更新几个地址的数据

当cpu执行存储指令时,它会首先试图将数据写到离cpu最近的L1_cache, 如果此时cpu出现L1未命中,则会访问下一级缓存。速度上L1_cache基本能和cpu持平,其他的均明显低于cpu,L2_cache的速度大约比cpu慢20-30倍,而且还存在L2_cache不命中的情况,又需要更多的周期去主存读取。其实在L1_cache未命中以后,cpu就会使用一个另外的缓冲区,叫做合并写存储缓冲区(WCBuffers)。这一技术称为合并写入技术

在请求L2_cache缓存行的所有权尚未完成时,cpu会把待写入的数据写入到合并写存储缓冲区.这个缓冲区允许cpu在写入或者读取该缓冲区数据的同时继续执行其他指令,这就缓解了cpu写数据时cache miss时的性能影响。

示例:
WCBuffers只有4个字节,runCaseOne一次填6个的话,要等下一次把剩下的2个填满。
而runCaseTwo一次填4个,就正好填满。

package character03;

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;  // 1Byte
            arrayA[slot] = b;  // 1Byte
            arrayB[slot] = b;  // 1Byte
            arrayC[slot] = b;  // 1Byte
            arrayD[slot] = b;  // 1Byte
            arrayE[slot] = b;  // 1Byte
            arrayF[slot] = b;  // 1Byte
        }
        return System.nanoTime() - start;
    }

    public static long runCaseTwo() {  // 一次正好写满一个四字节的 Buffer,比上面的循环效率更高
        long start = System.nanoTime();
        int i = ITERATIONS;
        while (--i != 0) {
            int slot = i & MASK;
            byte b = (byte) i; // 1Byte
            arrayA[slot] = b;  // 1Byte
            arrayB[slot] = b;  // 1Byte
            arrayC[slot] = b;  // 1Byte
        }
        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;
    }
}

乱序执行的证明

package character03;

public class T04_Disorder {
    private static int x = 0, y = 0;
    private static int a = 0, b = 0;
    // private static volatile int x = 0, y = 0;
    // private static volatile 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);
    }
}

如果一直都是顺序执行的,那么有以下这几种可能:

  1. a=1;x=b;b=1;y=a --> x=0,y=1
  2. a=1;b=1;x=b;y=a --> x=1,y=1
  3. a=1;b=1;y=a;x=b --> x=1,y=1
  4. b=1;y=a;a=1;x=b --> x=1,y=0
  5. b=1;a=1;y=a;x=b --> x=1,y=1
  6. b=1;a=1;x=b;y=a --> x=1,y=1

并且无论如何都不会出现x=0,y=0的情况,如果出现了,那就是出现了乱序执行的情况,即x=b发生在b=1之前,并且y=a发生在a=1之前

如何保证这种情况下不乱序

使用volatile

硬件层面

以 intel CPU 为例:
在这里插入图片描述
store(储存)
load(装载)
modify/mix(混合)
也可以使用原子指令,比如lock指令,lock是一个Full Barrier,执行时会锁住内存子系统来确保执行顺序,甚至跨多个CPU

软件层面比如JVM通常使用了内存屏障或原子指令来实现变量可见性和保持程序顺序

JVM层面

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值