JVM知识体系学习三:class文件初始化过程、硬件层数据一致性(硬件层)、缓存行、指令乱序执行问题、如何保证不乱序(volatile等)

news2025/1/8 6:00:15

文章目录

  • 前言
  • 一、class文件初始化过程
    • 1、概述
    • 2、初始化过程-案例1
      • a、代码T001_ClassLoadingProcedure 类加载过程
      • b、解析
    • 3、初始化过程-案例2
      • a、代码
      • b、解析
  • 二、单例模式-双重检查
  • 三、硬件层数据一致性
    • 1、硬件层的并发优化基础知识
      • b、Intel 的缓存一致性协议:MESI
  • 四、缓存行(面试可能会被问道)-伪共享
    • 1、定义
    • 2、案例
      • a、T01_CacheLinePadding
      • b、T02_CacheLinePadding
      • c、解析
  • 五、指令乱序执行问题
    • 1、乱序执行指令(并行)
    • 2、合并写操作
    • 3、合并写案例:WriteCombining
  • 六、指令乱序执行证明
    • 1、指令案例:T04_Disorder
    • 2、如何保证特定情况下不乱序
      • a、硬件内存屏障 Intel X86
      • b、JVM级别如何规范(JSR133)
    • 3、volatile的实现细节
      • a、字节码层面
      • b、JVM层面
      • c、OS和硬件层面
    • 4、synchronized实现细节
      • a. 字节码层面
        • i、小案例
        • ii、解析
      • b. JVM层面
      • c. OS和硬件层面

前言

一、class文件初始化过程

1、概述

在这里插入图片描述
上一篇博文主要讲的类初始化的类加载过程,也就是loading。
这里就说一下其他部分,通过案例进行讲解。

  1. loading ,class文件加载到内存
  2. linking,包括三部分,上过博文也说过了
  3. initializing,初始化部分

2、初始化过程-案例1

a、代码T001_ClassLoadingProcedure 类加载过程

package com.mashibing.jvm.c2_classloader;

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

class T {
    public static int count = 2; //0
    public static T t = new T(); // null

    //private int m = 8;

    private T() {
        count ++;
        //System.out.println("--" + count);
    }
}

在这里插入图片描述

b、解析

  1. loading过程:代码执行到 main 方法打印语句时,先加载T.class 类到内存中。
  2. verification: 检测过程
  3. preparation :静态变量 赋默认值过程,此时 count = 0, t = null
  4. resolution :解析过程
  5. initializing:初始化过程,此时 count = 2,t = new T(),执行无参构造函数,然后count自加为3 。所以输出3。

3、初始化过程-案例2

a、代码

还是上面案例1 的代码,改成两个静态变量的顺序,如下:
在这里插入图片描述
在这里插入图片描述
打印的结果为2,这是为啥呢。

b、解析

调换顺序,打印的数据就变成了2,还是初始化的过程,解说如下

  1. loading,类加载过程

  2. verification ,校验过程

  3. preparation ,静态变量赋默认值,此时 t=null,count = 0

  4. resolution ,解析过程

  5. initializing,赋值过程,此时 t=new T() 执行无参构造函数,此时 count 自加后为1,然后执行第二行,count = 2,则覆盖了第一次的count 的自加操作,所以为2

  6. 也说明类初始化过程中的重要性 以及 代码顺序的重要性。

  7. 当然,一般不会这么赋初值的,一般会用静态代码块或者在构造函数里进行 赋初值。都是为了面试而准备的,当然也是说明类初始化的顺序

二、单例模式-双重检查

DCL 单例,要加 volicate 关键字

三、硬件层数据一致性

JMM :java memory mode。java内存模型。

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

  • 存储器的层次结构 (深入理解计算机系统 原书第三版 P421)
    在这里插入图片描述
    在这里插入图片描述
  • 计算机模型CPU-内存模型:从下面这张图中可以看出,CPU 到内存直接还有多缓存(L1_cache、L2_cache、L3_cache),速度也是逐级递减(L1_cache基本能和cpu持平,其他的均明显低于cpu,L2_cache的速度大约比cpu慢20-30倍),L1_cache 和 L2_cache 是与CPU的内核在一块儿的,L3_cache 是共享的。
    在这里插入图片描述
  • 总线锁会锁住总线,使得其他CPU甚至不能访问内存中其他的地址,因而效率较低
    在这里插入图片描述

b、Intel 的缓存一致性协议:MESI

协议很多,Intel 的Cache(缓存)一致性协议:MESI。
可参考此网址:https://www.cnblogs.com/z00377750/p/9180644.html

在这里插入图片描述

  1. MESI:modified、Exclusive、Shared、Invalid
  2. 现代CPU的数据一致性实现 = 缓存锁(MESI …) + 总线锁

四、缓存行(面试可能会被问道)-伪共享

1、定义

  • 缓存行:当我们要把内存里面的某一些数据放到CPU自己的缓存时,不会只把这一个数据放进去,比如一个int型数据 12 ,只有 4 个字节,读缓存时不会只把这4个字节的数据读入到缓存,而且为了提高效率,把4个字节后面的一块儿内容全部读进去,读一个内容把一块儿内容全都读进去,这一块儿内容是一个基本的缓存单位,就是 缓存行,读取缓存以cache line为基本单位,目前64bytes。

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

  • 使用 缓存行 的对齐能够提高效率

在这里插入图片描述

2、案例

a、T01_CacheLinePadding

  1. 案例中, 数组开辟的两个数值的地址,应该在同一个缓存行。
  2. 通过两个线程去频繁的改变他们数值。
  3. 两个变量在一个缓存行,两个线程应该在两个CPU内核,那么会涉及到数据共享问题(上面说过Intel 的CPU 会用 MESI 缓存协议一致性)
  4. 因为涉及到缓存一致性,所以会导致时间会比较长
package com.mashibing.juc.c_001_02_FalseSharing;

public class T01_CacheLinePadding {
    public static long COUNT = 10_0000_0000L;

    private static class T {
        public long x = 0L; //8bytes
    }

    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 < COUNT; i++) {
                arr[0].x = i;
            }
        });

        Thread t2 = new Thread(() -> {
            for (long i = 0; i < COUNT; 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);
    }
}

b、T02_CacheLinePadding

  1. 案例中, 和上面的案例不同的是,一个数组。这里的是两个缓存行(两个缓存快),因为一个 Long 占 8个字节, 7个变量(p1-p7)就是 7*8 = 56个字节,在加一个 long 类型 变量x 正好是64个字节,缓存行的默认大小是 64个字节。
  2. 通过两个线程去频繁的改变他们数值。
  3. 两个变量在两个缓存行,两个线程应该在两个CPU内核,那么不会涉及到数据共享问题(上面说过Intel 的CPU 会用 MESI 缓存协议一致性)
  4. 因为涉及到缓存一致性,所以会导致时间会比较慢
package com.mashibing.juc.c_001_02_FalseSharing;

public class T02_CacheLinePadding {
    public static long COUNT = 10_0000_0000L;

    private static class Padding {
        private volatile long p1, p2, p3, p4, p5, p6, p7; // 7*8=56个字节
    }

    private static class T extends Padding {
        public volatile long x = 0L; //8bytes   8+56 = 64 个字节,所以 一个 T 就是一个缓存行
    }

    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 < COUNT; i++) {
                arr[0].x = i;
            }
        });

        Thread t2 = new Thread(() -> {
            for (long i = 0; i < COUNT; 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);
    }
}

c、解析

  1. 按照正常的设想,按照视频的教学,确实代码1要比代码2 的时间要长一些,因为涉及到了缓存一致性的问题。
  2. 但是我这里测试的确实代码1要比代码2的时间要短很多。可能会涉及到电脑类型、CPU内核数等等相关才照成这个问题的。

五、指令乱序执行问题

1、乱序执行指令(并行)

  • CPU为了提高指令执行效率,会在一条指令执行过程中(比如去内存读数据(去内存读数据要比CPU执行指令慢100倍)),去同时执行另一条指令,前提是,两条指令没有依赖关系
  • 参考资料:https://www.cnblogs.com/liushaodong/p/4777308.html

在这里插入图片描述

2、合并写操作

  • 参考资料: https://www.cnblogs.com/liushaodong/p/4777308.html

3、合并写案例:WriteCombining

该案例说明:合并写的速度快。

package com.mashibing.juc.c_029_WriteCombining;

/**
 * 原封不动老外的代码:合并写案例
 */
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;
    }
}

在这里插入图片描述

六、指令乱序执行证明

1、指令案例:T04_Disorder

x,y的值可能的取值:(1,0)(0,1)(1,1)
一旦出现了(0,0)说明出现了指令乱序执行。

package com.mashibing.jvm.c3_jmm;

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);
    }
}

在这里插入图片描述

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

a、硬件内存屏障 Intel X86

  • 加锁是肯定的可以的,但是在不同的CPU(很多CPU)都添加了 硬件内存屏障(CPU级别的内存屏障,和java的内存屏障无关系)* *

  • Intel 设置的比较简单,只有三条指令。

sfence(save fence 存栅栏的意思): store| 在sfence指令前的写操作当必须在sfence指令后的写操作前完成。
lfence(load fence 读屏障):load | 在lfence指令前的读操作当必须在lfence指令后的读操作前完成。
mfence(二者之和):modify/mix | 在mfence指令前的读写操作当必须在mfence指令后的读写操作前完成。

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

b、JVM级别如何规范(JSR133)

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

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

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

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

  • 上面的JVM级别的四个指令屏障 都是依赖于 硬件去实现的,硬件的实现不是只有内存屏障能够实现JVM级别的内存屏障,即JVM的内存屏障或者交JVM级别的有序性,它的硬件级别的实现并不一定依赖于硬件级别的内存屏障,还依赖于硬件级别的 lock 指令

  • 硬件界别的和JVM级别不是一回事儿。

3、volatile的实现细节

volatile 的实现过程,分不同的层面,包含以下三个层面。

  1. .java 编译成 字节码(byte code) .class 这是字节码层面
  2. 字节码 在JVM层级实现,这是 JVM层面
  3. JVM 交付到 硬件 去执行,这是 OS硬件层面

a、字节码层面

  1. ACC_VOLATILE
  2. 只需要一个 volatile 关键字即可
  3. 访问标志(access flag):就是修饰符,比如 public、private、protect等。
    在这里插入图片描述

b、JVM层面

到了JVM屏障,volatile内存区的读写 都加屏障

StoreStoreBarrier
volatile 写操作
StoreLoadBarrier
LoadLoadBarrier
volatile 读操作
LoadStoreBarrier

c、OS和硬件层面

https://blog.csdn.net/qq_26222859/article/details/52235930
hsdis 工具 - HotSpot Dis Assembler
在 windows 使用 lock 指令实现 | MESI 实现

4、synchronized实现细节

同 volatile 一样,也是三个层级。

  1. 字节码层面
  2. JVM层面
  3. OS 硬件层面

a. 字节码层面

从下面截图中可以看出,使用的是 ACC_SYNCHRONIZED 修饰符。
monitorenter monitorexit

i、小案例

package com.mashibing.jvm.c3_jmm;

public class TestSync {
    synchronized void m() {

    }

    void n() {
        synchronized (this) {

        }
    }

    public static void main(String[] args) {

    }
}

从编译后的字节码看到如下图
在这里插入图片描述

ii、解析

synchronized 块 中可以看出,有三个指令, monitorenter,monitorexit,monitorexit。后两个是一个指令。

为什么会三个,后两个是一样的呢,逻辑如下图,第三个指令是如果出现了异常进行捕捉,才用到第三个 monitorexit 指令。
在这里插入图片描述

b. JVM层面

是C 和 C++ 写的,则 C 和 C++ 调用了操作系统提供的同步机制。

c. OS和硬件层面

CPU Intel X86 : 使用 lock cmpxchg / xxx 等指令。
https://blog.csdn.net/21aspnet/article/details/88571740

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/146581.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

Vivado综合设置之-keep_equivalent_registers

-keep_equivalent_registers即保留等效寄存器&#xff0c;所谓等效寄存器是指共享输入端口&#xff08;输入时钟端口clk和输入数据端口rst&#xff09;的寄存器。 勾选它时&#xff0c;意味着Vivado不会对等效寄存器进行优化&#xff1b; 不勾选它时&#xff08;默认情况&…

eclipse安装UML插件

安装AmaterasUML AmaterasUML 是一个用于 Eclipse 的轻量级 UML 和 ER 图编辑器。 将AmaterasUML的3个jar包拷到Eclpise的plugins文件下&#xff1a; 重启eclipse 在新建菜单中可以发现已经出现了UML文件选项 安装GEF插件&#xff08;Eclipse2018-12 以后无需安装&#xf…

②电子产品拆解分析-电动牙刷

②电子产品拆解分析-电动牙刷一、功能介绍二、电路分析以及器件作用1、振动电机开关控制电路2、锂电池供电与充电电路三、本产品的优缺点1、优点&#xff1a;2、缺点&#xff1a;一、功能介绍 ①5档工作模式&#xff1b;②2分钟倒计时停止工作&#xff1b;③工作续航一个星期以…

【MySQL】详解索引操作

索引什么是索引&#xff1f;索引的优势和劣势索引类型按数据结构分类按物理存储分类按字段特性分类主键索引唯一索引普通索引全文索引前缀索引按字段个数分类索引操作创建索引创建主键索引唯一索引的创建普通索引的创建全文索引的创建explain工具查询索引删除索引索引最好设置为…

SQL 注入学习路线

学习路线&#xff08;大致&#xff09; HTML > SQL > Python > SQL 注入&#xff08;使用 sqli-labs 靶场来学习 SQL 注入&#xff09; HTML 视频 【前端开发入门教程&#xff0c;web前端零基础html5 css3前端项目视频教程】 要求 使用该视频进行 HTML 基础部分…

Python之字符串的特点

1.布尔值 Python2中没有布尔值&#xff0c;直接用数字0表示Flase&#xff0c;用数字1表示True。Python3中&#xff0c;把True和False定义成了关键字&#xff0c;但他们的本质还是1和0&#xff0c;甚至可以和数字相加。 >>> a True >>> b 3 >>> …

[多图,秒懂]如何训练一个“万亿大模型”?

1. 背景近几年&#xff0c;随着“大模型”概念的提出&#xff0c;深度学习模型越来越大&#xff0c;如何训练这些大模型成为一个亟待解决的工程问题。最初的视觉模型只有几百兆的参数量&#xff0c;而现在的语言模型中&#xff0c;动则百亿&#xff0c;千亿的参数量&#xff0c…

[golang工作日记] for range 踩坑

1、for range指针赋值 Ops的数据保存在两个表中&#xff0c;一个是ops_tab&#xff0c;另一个是staff_tab&#xff0c;其中ops_tab的staff_id是staff_tab的外键&#xff0c;两个表都有staff_id字段。 type OpsDetail struct {OpsId stringStaffId stringOps *model.Ops…

Node.js安装配置

目录1. 下载node2. 安装3. 检查是否安装成功4. 配置缓存路径5. 实现一个demo1. 下载node 官方地址https://nodejs.org/en/下载长期支持版本 2. 安装 一路next node一起安装npm 这个地方不要勾选 安装成功 3. 检查是否安装成功 cmd运行下面命令 # 查看node的版本 node -v # …

【博客571】“时序敏感应用“ 如何prometheus自定义上报时间戳

“时序敏感应用” 如何prometheus自定义上报时间戳 1、场景 在物理网络监控中&#xff0c;对于流量趋势是极其敏感的&#xff0c;物理网络监控流量的点通常是秒级别甚至毫秒级别&#xff0c;此时这些时许点通过各种上报上传到监控系统中&#xff0c;由于网络波动&#xff0c;可…

Kubernetes v1.24.2高可用部署

sskubeasz 1、Kubeasy简介 kubeasz 致力于提供快速部署高可用k8s集群的工具, 同时也努力成为k8s实践、使用的参考书&#xff1b;基于二进制方式部署和利用ansible-playbook实现自动化&#xff1b;既提供一键安装脚本, 也可以根据安装指南分步执行安装各个组件。 kubeasz 从每…

【lc刷题 day12】堆/栈/队列

BM42 用两个栈实现队列 easy import java.util.Stack;public class Solution {Stack<Integer> stack1 new Stack<Integer>();Stack<Integer> stack2 new Stack<Integer>();public void push(int node) {stack1.push(node);}public int pop() {if(st…

随便聊聊浪潮开务数据库

今天这个话题挺随意&#xff0c;我们来聊聊浪潮开务数据库&#xff0c;原因主要是我的微信朋友圈被这个数据库刷屏了。当然我对这款号称多模数据库的非开源数据库也很感兴趣&#xff0c;也有很多疑问&#xff0c;希望各位专家能帮忙答疑解惑&#xff0c;揭开这款即将发布的 Kai…

机器学习--多层感知机、卷积神经网络、循环神经网络

目录 一、多层感知机 二、卷积神经网络 三、循环神经网络 总结 一、多层感知机 手工提取特征&#xff08;用人的知识进行&#xff09; --> 神经网络来提取特征。 神经网络&#xff08;可能更懂机器学习&#xff09;来提取 可能对后面的线性或softmax回归可能会更好一…

【UE4 第一人称射击游戏】23-添加子弹伤害

上一篇&#xff1a;https://blog.csdn.net/ChaoChao66666/article/details/128589063?spm1001.2014.3001.5501本篇效果&#xff1a;步骤&#xff1a;创建一个蓝图类&#xff08;父类为Character&#xff09;&#xff0c;命名为“SimpleAI”双击打开“SimpleAI”&#xff0c;点…

非对称加密实战(一):JDK生成keystore获取公钥私钥及代码验证【附源码】

目录使用说明非对称加密生成keystore文件公钥私钥互相解密获取fd-alias.keystore中的公钥私钥使用生成公钥私钥进行解密源码地址使用说明 非对称加密 非对称加密算法主要有&#xff1a;RSA、Elgamal、背包算法、Rabin、D-H、ECC&#xff08;椭圆曲线加密算法&#xff09;。下…

TensorRT学习笔记--基于FCN-ResNet101推理引擎实现语义分割

目录 前言 1--Pytorch模型转换为Onnx模型 2--Onnx模型可视化及测试 2-1--可视化Onnx模型 2-2--测试Onnx模型 3--Onnx模型转换为Tensor RT推理模型 4--基于Tensor RT使用推理引擎实现语义分割 前言 基于Tensor RT的模型转换流程&#xff1a;Pytorch → Onnx → Tensor RT…

通用vue组件化首页

一、首先先建立文件main.vue,构建主体 1.选择合适的模板element-plus,直接复制 2.编写相应的样式 <template><div class"main"><el-container class"main-content"><el-aside> aside </el-aside><el-container class&q…

2022年中职组网络安全竞赛D模块竞赛漏洞报告单总结

Windows加固 后门用户 漏洞发现过程 打开cmd使用net user 看到”hacker”用户,疑似存在后门用户 使用hacker/123456成功登录目标服务器,证明存在后门用户 漏洞加固过程 删除后门用户

HTML与CSS基础(一)—— HTML基础(web标准、开发工具、标签)

目标能够理解HTML的 基本语法 和标签的关系 能够使用 排版标签 实现网页中标题、段落等效果 能够使用 相对路径 选择不同目录下的文件 能够使用 媒体标签 在网页中显示图片、播放音频和视频 能够使用 链接标签 实现页面跳转功能一、基础认知目标&#xff1a;认识 网页组成 和 五…