• 通俗易懂Java内存模型详解面试带例子


    一、前言

    本文主要的脑图如下:
    在这里插入图片描述
    如果上面图片太模糊,请打开下面的链接:
    java内存模型脑图

    本篇文章按照自顶向下的方式将,先讲使用场景,再讲用法,最后讲原理。


    在这里插入图片描述

    二、Java代码转化成CPU指令过程

    1. 最开始,我们编写的Java代码,是*java文件
    2. 在编译(javac命令)后,从刚才的java文件会变出一个新的Java字节码文件(.class)
    3. JVM会执行刚才生成的字节码文件(*class),并把字节码文件转化为机器指令
    4. 机器指令可以直接在CPU上执运行,也就是最终的程序执行

    三、jvm内存结构、Java内存模型、Java对象模型对比

    备注:jvm内存结构和Java对象模型不做具体展开。

    jvm内存结构,和Java虚拟机的运行时区域有关
    Java内存模型,和并发编程有关
    Java对象模型,和Java对象在虚拟机的表现形式有关

    jvm内存结构
    在这里插入图片描述
    本篇文章不是主要讲jvm内存结构的,因此简单提一下,不再做具体讲述。

    Java对象模型
    在这里插入图片描述

    1. Java对象自身的存储模型
    2. JVM会给这个类创建一个instanceKlass,保存在方法区,用来在JVM层表示该Java类。
    3. 当我们在Java代码中,使用new创建一个对象的时候,JVM会创建 instanceOopDesc对象,这个对象中包含了对象头以及实例数据。

    四、JMM(Java内存模型)

    1. 为什么需要JMM?

    C语言不存在内存模型的概念,依赖处理器,不同处理器结果不一样,无法保证并发安全,因此需要一个标准,让多线程运行的结果可预期。

    2. JMM是什么?

    1. 是一组规范,需要各个jvm的实现遵守JMM规范,以便开发者利用这些规范,方便开发多线程程序。
    2. JMM是工具类和关键字的原理,volatile、synchronized、Lock等的原理都是JMM。如果没有JMM,那就需要我们自己指定什么时候用内存栅栏等,那是相当麻烦的,幸好有了JMM,让我们只需要用同步工具和关键字就可以开发并发程序。

    JMM的重要特性是重排序、原子性、可见性,下面几章将挨个介绍。

    五、重排序

    1.重排序例子

    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;
    
                CountDownLatch latch = new CountDownLatch(3);
    
                Thread one = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            latch.countDown();
                            latch.await();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        a = 1;
                        x = b;
                    }
                });
                Thread two = new Thread(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            latch.countDown();
                            latch.await();
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        b = 1;
                        y = a;
                    }
                });
                two.start();
                one.start();
                latch.countDown();
                one.join();
                two.join();
    
                String result = "第" + i + "次(" + x + "," + y + ")";
                if (x == 0 && y == 0) {
                    System.out.println(result);
                    break;
                } else {
                    System.out.println(result);
                }
            }
        }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55

    以上运行结果有以下3中结果外

    1. a=1;x=b(0);b=1;y=a(1),最终结果是x=0 y=1
    2. b=1;y=a(0);a=1;x=b(1),最终结果是x=1 y=0
    3. b=1;a=1;x=b(1);y=a(1),最终结果是x=1 y=1

    还有第四种情况:
    在这里插入图片描述
    这就是重排序导致的

    2.什么是重排序

    在线程1内部的两行代码的实际执行顺序和代码在Java文件中的顺序不一致,代码指令并不是严格按照代码语句顺序执行的它们的顺序被改变了,这就是重排序,这里被颠倒的是y=a和b=1这两行语句。

    3.重排序好处

    提高处理速度
    在这里插入图片描述

    如上图所示,重排序a少了一次读取和写入,因此提高了响应速度。

    4.重排序的3种情况

    1. 编译器优化:包括JVM,JIT编译器等
    2. CPU指令重排:就算编译器不发生重排,CPU也可能对指令进行重排
    3. 内存的“重排序”:线程A的修改线程B却看不到,引出可见性问题

    六、可见性

    1.可见性例子:

    	/**
     * 描述:     演示可见性带来的问题
     */
    public class FieldVisibility {
    
        int a = 1;
        int b = 2;
    
        private void change() {
            a = 3;
            b = a;
        }
    
    
        private void print() {
            System.out.println("b=" + b + ";a=" + a);
        }
    
        public static void main(String[] args) {
            while (true) {
                FieldVisibility test = new FieldVisibility();
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            Thread.sleep(1);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        test.change();
                    }
                }).start();
    
                new Thread(new Runnable() {
                    @Override
                    public void run() {
                        try {
                            Thread.sleep(1);
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                        }
                        test.print();
                    }
                }).start();
            }
    
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48

    出现的情况:
    a=3,b=2
    a=1,b=2
    a=3,b=3

    除了以上3种情况,还有
    b=3,a=1这种情况
    这就是可见性带来的问题
    在这里插入图片描述

    每个线程都有一个工作内存,修改完之后,不一定立即同步回主内存,这样其他线程读取的数据可能不同步,导致出现可见性问题,使用volatile可以保证可见性。

    2.为什么会有可见性的问题

    如下图:
    从下到上分别是主存、l3缓存、l2缓存、…寄存器、cpu,因为cpu很快,主存空间很大,速度慢,如果直接从主存里拿,速度很慢,这时候划分了很多缓存,从下往上缓存逐渐减少,但速度增快,最后cpu(比如core1)从registers寄存器中拿值,速度很快。别的线程修改完数据,不一定同步马上回了主存,可能同步到了某级缓存里,这就导致别的线程拿的值不是最新的。

    CPU有多级缓存,导致读的数据过期
    高速缓存的容量比主内存小,但是速度仅次于寄存器,所以在CPU和主内存之间就多了Cache层
    线程间的对于共享变量的可见性问题不是直接由多核引起的,而是由多缓存引起的。

    如果所有个核心都只用一个缓存,那么也就不存在内存可见性问题了。
    每个核心都会将自己需要的数据读到独占缓存中,数据修改后也是写入到缓存中,然后等待刷入到主存中。所以会导致有些核心读取的值是一个过期的值。

    在这里插入图片描述

    3.JMM的抽象 可见性

    Java作为高级语言,屏蔽了这些底层细节,用JMM定义了一套读写内存数据的规范,虽然我们不再需要关心一级缓存和二级缓存的问题,但是,JMM抽象了主内存和本地内存的概念。
    这里说的本地内存并不是真的是一块给每个线程分配的内存,而是 JMM的一个抽象,是对于寄存器、一级缓存、二级缓存等的抽象。

    JMM规定:
    1.所有的变量都存储在主内存中,同时每个线程也有自己独立的工作内存工作内存中的变量内容是主内存中的拷贝
    2.线程不能直接读写主内存中的变量,而是只能操作自己工作内存中的变量然后再同步到主内存中
    3.主内存是多个线程共享的,但线程间不共享工作内存,如果线程间需要通信,必须借助主内存中转来完成.
    所有的共享变量存在于主内存中,每个线程有自己的本地内存,而且线程读写共享数据也是通过本地内存交换的,这也就导致了可见性的问题。

    4.happens-Before原则

    1.什么是happens-Before

    happens-before规则是用来解决可见性问题的:在时间上,动作A发生在动作B之前,B保证能看见A,这就是happens-before。

    两个操作可以用happens-before来确定它们的执行顺序:如果一个操作 happens-before 于另一个操作,那么我们说第一个操作对于第二个操作是可见的。

    2.什么不是happens-Before

    两个线程没有相互配合的机制,所以代码X和Y的执行结果并不能保证总被对方看到的,这就不具备happens-before。

    3.happens-Before 规则有哪些

    1. 锁操作
      在这里插入图片描述

    比如lock,synchronized等,如上图所示。
    在这里插入图片描述
    2. volatile
    在这里插入图片描述
    只要完成了写线程操作,就一定能被读取到,包括之前的一些操作。比如a=3;b=a 对b加volatile,那么另一个线程读取的时候,a=3,b=3,那么b一定能被另一个线程读取到,a=3,b=3.这种情况a一定等于3。

    1. 还有比如线程join、线程中断、线程安全的容器get一定能看到之前put的动作
  • 相关阅读:
    【React】精选5题
    【pytest】 标记冒烟用例 @pytest.mark.smoke
    SpringBoot项目中使用MultipartFile来上传文件(包含多文件)
    Part7:Pandas 的SettingWithCopyWarning 报警复现、原因、解决方案
    相机相关:相机模型与畸变模型
    富文本编辑器vue-quill-editor的使用(可上传视频、上传图片及图片的放大缩小)
    Python 自动化教程(1) 概述,第一篇 Excel自动化首篇
    什么是LRU算法
    Python Tkinter Multiple Windows 教程
    CLIP改进工作串讲(上)
  • 原文地址:https://blog.csdn.net/qq_41346335/article/details/126206084