java内存模型(代码片段)

爱上口袋的天空 爱上口袋的天空     2022-12-11     248

关键词:

1、简介

       Java内存模型规范了Java虚拟机与计算机内存是如何协同工作的。Java虚拟机是一个完整的计算机的一个模型,因此这个模型自然也包含一个内存模型——又称为Java内存模型。

       如果你想设计表现良好的并发程序,理解Java内存模型是非常重要的。Java内存模型规定了如何和何时可以看到由其他线程修改过后的共享变量的值,以及在必须时如何同步的访问共享变量。

        原始的Java内存模型存在一些不足,因此Java内存模型在Java1.5时被重新修订。这个版本的Java内存模型在Java8中人在使用。

2、Java内存模型内部原理

Java内存模型把Java虚拟机内部划分为线程栈和堆。这张图演示了Java内存模型的逻辑视图。

        每一个运行在Java虚拟机里的线程都拥有自己的线程栈。这个线程栈包含了这个线程调用的方法当前执行点相关的信息。一个线程仅能访问自己的线程栈。一个线程创建的本地变量对其它线程不可见,仅自己可见。即使两个线程执行同样的代码,这两个线程任然在在自己的线程栈中的代码来创建本地变量。因此,每个线程拥有每个本地变量的独有版本。

        所有原始类型的本地变量都存放在线程栈上,因此对其它线程不可见。一个线程可能向另一个线程传递一个原始类型变量的拷贝,但是它不能共享这个原始类型变量自身。 

        堆上包含在Java程序中创建的所有对象,无论是哪一个对象创建的。这包括原始类型的对象版本。如果一个对象被创建然后赋值给一个局部变量,或者用来作为另一个对象的成员变量,这个对象任然是存放在堆上。

         下面这张图演示了调用栈和本地变量存放在线程栈上,对象存放在堆上。

  •  一个本地变量可能是原始类型,在这种情况下,它总是“呆在”线程栈上。
  • 一个本地变量也可能是指向一个对象的一个引用。在这种情况下,引用(这个本地变量)存放在线程栈上,但是对象本身存放在堆上。
  • 一个对象可能包含方法,这些方法可能包含本地变量。这些本地变量任然存放在线程栈上,即使这些方法所属的对象存放在堆上。
  • 一个对象的成员变量可能随着这个对象自身存放在堆上。不管这个成员变量是原始类型还是引用类型。
  • 静态成员变量跟随着类定义一起也存放在堆上
  • 存放在堆上的对象可以被所有持有对这个对象引用的线程访问。当一个线程可以访问一个对象时,它也可以访问这个对象的成员变量。如果两个线程同时调用同一个对象上的同一个方法,它们将会都访问这个对象的成员变量,但是每一个线程都拥有这个本地变量的私有拷贝。

    下图演示了上面提到的点:

两个线程拥有一些列的本地变量。其中一个本地变量(Local Variable 2)执行堆上的一个共享对象(Object 3)。这两个线程分别拥有同一个对象的不同引用。这些引用都是本地变量,因此存放在各自线程的线程栈上。这两个不同的引用指向堆上同一个对象。 

注意,这个共享对象(Object 3)持有Object2和Object4一个引用作为其成员变量(如图中Object3指向Object2和Object4的箭头)。通过在Object3中这些成员变量引用,这两个线程就可以访问Object2和Object4。

这张图也展示了指向堆上两个不同对象的一个本地变量。在这种情况下,指向两个不同对象的引用不是同一个对象。理论上,两个线程都可以访问Object1和Object5,如果两个线程都拥有两个对象的引用。但是在上图中,每一个线程仅有一个引用指向两个对象其中之一。

因此,什么类型的Java代码会导致上面的内存图呢?如下所示:

public class MyRunnable implements Runnable() 

    public void run() 
        methodOne();
    

    public void methodOne() 
        int localVariable1 = 45;

        MySharedObject localVariable2 =
            MySharedObject.sharedInstance;

        //... do more with local variables.

        methodTwo();
    

    public void methodTwo() 
        Integer localVariable1 = new Integer(99);

        //... do more with local variable.
    



public class MySharedObject 

    //static variable pointing to instance of MySharedObject

    public static final MySharedObject sharedInstance =
        new MySharedObject();


    //member variables pointing to two objects on the heap

    public Integer object2 = new Integer(22);
    public Integer object4 = new Integer(44);

    public long member1 = 12345;
    public long member1 = 67890;

如果两个线程同时执行run()方法,就会出现上图所示的情景。run()方法调用methodOne()方法,methodOne()调用methodTwo()方法。

methodOne()声明了一个原始类型的本地变量和一个引用类型的本地变量。

每个线程执行methodOne()都会在它们对应的线程栈上创建localVariable1localVariable2的私有拷贝。localVariable1变量彼此完全独立,仅“生活”在每个线程的线程栈上。一个线程看不到另一个线程对它的localVariable1私有拷贝做出的修改。

每个线程执行methodOne()时也将会创建它们各自的localVariable2拷贝。然而,两个localVariable2的不同拷贝都指向堆上的同一个对象。代码中通过一个静态变量设置localVariable2指向一个对象引用。仅存在一个静态变量的一份拷贝,这份拷贝存放在堆上。因此,localVariable2的两份拷贝都指向由MySharedObject指向的静态变量的同一个实例。MySharedObject实例也存放在堆上。它对应于上图中的Object3。

注意,MySharedObject类也包含两个成员变量。这些成员变量随着这个对象存放在堆上。这两个成员变量指向另外两个Integer对象。这些Integer对象对应于上图中的Object2和Object4.

注意,methodTwo()创建一个名为localVariable的本地变量。这个成员变量是一个指向一个Integer对象的对象引用。这个方法设置localVariable1引用指向一个新的Integer实例。在执行methodTwo方法时,localVariable1引用将会在每个线程中存放一份拷贝。这两个Integer对象实例化将会被存储堆上,但是每次执行这个方法时,这个方法都会创建一个新的Integer对象,两个线程执行这个方法将会创建两个不同的Integer实例。methodTwo方法创建的Integer对象对应于上图中的Object1和Object5。

还有一点,MySharedObject类中的两个long类型的成员变量是原始类型的。因为,这些变量是成员变量,所以它们任然随着该对象存放在堆上,仅有本地变量存放在线程栈上。

注意:

一、成员变量(实例变量,属性)

1.1-成员变量:(在类中定义,    访问修饰符   修饰符   type  name  = value)

1.2-什么是成员变量?

  成员变量就是类中的属性。当new对象的时候,每个对象都有一份属性。一个对象中的属性就是成员变量。

1.3-作用范围?

  在类内部,任何地方都可以访问成员变量。

1.4- 生命周期?(在内存中存在的时间)

  出生: new对象的时候,开辟内存空间。 

  死亡:  堆内存地址没有引用,变成垃圾,被垃圾回收器回收后。

二、局部变量(本地变量)

2.1-局部变量:(修饰符   type  name = value)

2.2- 什么是局部变量?

  方法的形式参数以及在方法中定义的变量。

2.3-作用范围?

  形参:在方法体中任何位置都可以访问。

  方法中定义变量:从定义处开始,直到所在代码块结束。

2.4  生命周期?(在内存中存在的时间)

  出生:运行到创建变量的语句时。 

  死亡:超过了其作用范围。总结:

三、类变量(静态属性)

3.1-类变量:(访问修饰符  static  type  name = value)

3.2-什么是类变量?

  被static修饰的属性。

3.3-作用范围?

  在类变量定义之后。

3.4- 生命周期?(在内存中存在的时间)

  出生:类加载时,类变量就分配内存空间。 

  死亡:JVM退出

3、硬件内存架构

现代硬件内存模型与Java内存模型有一些不同。理解内存模型架构以及Java内存模型如何与它协同工作也是非常重要的。这部分描述了通用的硬件内存架构,下面的部分将会描述Java内存是如何与它“联手”工作的。

下面是现代计算机硬件架构的简单图示:

一个现代计算机通常由两个或者多个CPU。其中一些CPU还有多核。从这一点可以看出,在一个有两个或者多个CPU的现代计算机上同时运行多个线程是可能的。每个CPU在某一时刻运行一个线程是没有问题的。这意味着,如果你的Java程序是多线程的,在你的Java程序中每个CPU上一个线程可能同时(并发)执行。

每个CPU都包含一系列的寄存器,它们是CPU内内存的基础。CPU在寄存器上执行操作的速度远大于在主存上执行的速度。这是因为CPU访问寄存器的速度远大于主存。

每个CPU可能还有一个CPU缓存层。实际上,绝大多数的现代CPU都有一定大小的缓存层。CPU访问缓存层的速度快于访问主存的速度,但通常比访问内部寄存器的速度还要慢一点。一些CPU还有多层缓存,但这些对理解Java内存模型如何和内存交互不是那么重要。只要知道CPU中可以有一个缓存层就可以了。

一个计算机还包含一个主存。所有的CPU都可以访问主存。主存通常比CPU中的缓存大得多。

通常情况下,当一个CPU需要读取主存时,它会将主存的部分读到CPU缓存中。它甚至可能将缓存中的部分内容读到它的内部寄存器中,然后在寄存器中执行操作。当CPU需要将结果写回到主存中去时,它会将内部寄存器的值刷新到缓存中,然后在某个时间点将值刷新回主存。

当CPU需要在缓存层存放一些东西的时候,存放在缓存中的内容通常会被刷新回主存。CPU缓存可以在某一时刻将数据局部写到它的内存中,和在某一时刻局部刷新它的内存。它不会再某一时刻读/写整个缓存。通常,在一个被称作“cache lines”的更小的内存块中缓存被更新。一个或者多个缓存行可能被读到缓存,一个或者多个缓存行可能再被刷新回主存。 

4、Java内存模型和硬件内存架构之间的桥接

上面已经提到,Java内存模型与硬件内存架构之间存在差异。硬件内存架构没有区分线程栈和堆。对于硬件,所有的线程栈和堆都分布在主内中。部分线程栈和堆可能有时候会出现在CPU缓存中和CPU内部的寄存器中。如下图所示:

当对象和变量被存放在计算机中各种不同的内存区域中时,就可能会出现一些具体的问题。主要包括如下两个方面:

-线程对共享变量修改的可见性
-当读,写和检查共享变量时出现race conditions

下面我们专门来解释以下这两个问题。

 共享对象可见性

如果两个或者更多的线程在没有正确的使用volatile声明或者同步的情况下共享一个对象,一个线程更新这个共享对象可能对其它线程来说是不接见的。

想象一下,共享对象被初始化在主存中。跑在CPU上的一个线程将这个共享对象读到CPU缓存中。然后修改了这个对象。只要CPU缓存没有被刷新会主存,对象修改后的版本对跑在其它CPU上的线程都是不可见的。这种方式可能导致每个线程拥有这个共享对象的私有拷贝,每个拷贝停留在不同的CPU缓存中。

下图示意了这种情形。跑在左边CPU的线程拷贝这个共享对象到它的CPU缓存中,然后将count变量的值修改为2。这个修改对跑在右边CPU上的其它线程是不可见的,因为修改后的count的值还没有被刷新回主存中去。

解决这个问题你可以使用Java中的volatile关键字。volatile关键字可以保证直接从主存中读取一个变量,如果这个变量被修改后,总是会被写回到主存中去。

Race Conditions (也叫做资源竞争

如果两个或者更多的线程共享一个对象,多个线程在这个共享对象上更新变量,就有可能发生race conditions

想象一下,如果线程A读一个共享对象的变量count到它的CPU缓存中。再想象一下,线程B也做了同样的事情,但是往一个不同的CPU缓存中。现在线程A将count加1,线程B也做了同样的事情。现在count已经被增在了两个,每个CPU缓存中一次。

如果这些增加操作被顺序的执行,变量count应该被增加两次,然后原值+2被写回到主存中去。

然而,两次增加都是在没有适当的同步下并发执行的。无论是线程A还是线程B将count修改后的版本写回到主存中取,修改后的值仅会被原值大1,尽管增加了两次。

下图演示了上面描述的情况:

 解决这个问题可以使用Java同步块。一个同步块可以保证在同一时刻仅有一个线程可以进入代码的临界区。同步块还可以保证代码块中所有被访问的变量将会从主存中读入,当线程退出同步代码块时,所有被更新的变量都会被刷新回主存中去,不管这个变量是否被声明为volatile。

java内存模型(代码片段)

Java内存模型Java内存模型硬件内存模型多CUP缓存产生的同步问题CPU指令重排Java内存模型线程通信中可能存在的问题并发三要素可见性原子性有序性Java内存模型参考视频:https://www.bilibili.com/video/BV1F64y1B7sV参考博客:https://z... 查看详情

重新认识java内存模型(jmm)(代码片段)

通过学习《深入理解Java虚拟机》有关Java内存模型的介绍,整理的学习笔记,供你参考。文章目录Java内存模型定义主内存和工作内存CPU普及内存间交互操作对于volatile型变量的特殊规则原子性、可见性与有序性先行发生&#x... 查看详情

java并发编程:java内存模型(代码片段)

一、Java内存模型基础1.两个关键问题2.Java内存模型的抽象结构3.指令序列的重排序4.并发编程模型的分类5.happens-before二、指令重排序1.数据依赖性2.as-if-serial语义3.重排序对多线程的影响三、顺序一致性内存模型1、数据竞争与顺序... 查看详情

浅谈jvm内存结构,java内存模型和java对象模型(代码片段)

Java虚拟机正文开始@Assassin目录Java虚拟机1.JVM内存结构:1.1Java虚拟机栈:1.2堆:1.3方法区:2.Java内存模型:3.Java对象模型:4.三者区别:1.JVM内存结构:Java代码是要运行在Java虚拟机上的,而虚拟机在执行Java程序的过程中会... 查看详情

全面理解java内存模型(代码片段)

Java内存模型即JavaMemoryModel,简称JMM。JMM定义了Java虚拟机(JVM)在计算机内存(RAM)中的工作方式。JVM是整个计算机虚拟模型,所以JMM是隶属于JVM的。如果我们要想深入了解Java并发编程,就要先理解好Java内存模型。Java内存... 查看详情

从cpu说起,深入理解java内存模型!(代码片段)

Java内存模型,许多人会错误地理解成JVM的内存模型。但实际上,这两者是完全不同的东西。Java内存模型定义了Java语言如何与内存进行交互,具体地说是Java语言运行时的变量,如何与我们的硬件内存进行交互的。... 查看详情

java内存模型和线程安全(代码片段)

Java内存模型和线程安全Java内存模型引言volatile关键字synchronized关键字Java线程Java线程安全synchronized锁优化锁优化技巧列举自旋锁锁消除锁粗化具体实现轻量级锁偏向锁Java内存模型引言对于多核处理器而言,每个核都会有自己单独... 查看详情

jvm内存结构java内存模型和java对象模型(代码片段)

Java作为一种面向对象的,跨平台语言,其对象、内存等一直是比较难的知识点。而且很多概念的名称看起来又那么相似,很多人会傻傻分不清楚。比如本文要讨论的JVM内存结构、Java内存模型和Java对象模型,这就是三个截然不同... 查看详情

java-内存模型(jsr-133)(代码片段)

Java内存模型(JavaMemoryModel,JMM)看上去和Java内存结构(JVM运行时内存结构)差不多,但这两者并不是一回事。JMM并不像JVM内存结构一样是真实存在的,它只是一个抽象的概念。Java的线程间通过共享内存(Java堆和方法区)进行... 查看详情

java内存模型并发三个特性(代码片段)

目录一.内存模型的相关概念2.并发编程中的三个概念1.原子性2.可见性3.有序性3.java内存模型1.原子性2.可见性3.有序性https://www.cnblogs.com/dolphin0520/p/3920373.html此前需要了解简单Thread实现一.内存模型的相关概念计算机在执行程序的时... 查看详情

java内存模型(代码片段)

Java内存模型规范了JVM如何提供按需禁用缓存和编译优化的方法。具体来说,这些方法包括volatile、synchronized和final三个关键字,以及六项Happens-Before规则。Happens-Before的7个规则:(1).程序次序规则:在一个线程内,按照程序代码顺... 查看详情

十分钟吃透java内存模型(代码片段)

十分钟吃透Java内存模型Java内存模型把Java虚拟机内部划分为线程栈和堆。这张图演示了Java内存模型的逻辑视图。每个运行在JVM里的线程都拥有自己的线程栈。线程栈包含了该线程调用的方法的当前执行点相关的信息。一个线程... 查看详情

浅谈java的内存模型以及交互(代码片段)

本文的内存模型只写虚拟机内存模型,物理机的不予描述。 Java内存模型  在Java中,虚拟机将运行时区域分成6中,如下图:            程序计数器:用来记录当前线程执行到哪一步操作。... 查看详情

面试官:说说什么是java内存模型(jmm)?(代码片段)

本文禁止转载1.为什么要有内存模型?1.1.硬件内存架构1.2.缓存一致性问题1.3.处理器优化和指令重排序2.并发编程的问题3.Java内存模型3.1.Java运行时内存区域与硬件内存的关系3.2.Java线程与主内存的关系3.3.线程间通信4.有态度... 查看详情

javaconcurrenthashmap(代码片段)

Java内存模型由于ConcurrentHashMap是建立在Java内存模型基础上的,为了更好的理解ConcurrentHashMap,让我们首先来了解一下Java的内存模型。Java语言的内存模型由一些规则组成,这些规则确定线程对内存的访问如何排序以及... 查看详情

java内存模型(代码片段)

...序的实体是线程,每个线程JVM都会为其创建一个工作内存,工作内存是每个线程的的私有数据区域,java内存模型规定中的变量都存储在主内存& 查看详情

从硬件缓存模型到java内存模型原理浅析(代码片段)

...r?一、硬件方面的问题1、背景在现代系统的CPU中,所有的内存访问都是通过层层缓存进行的。CPU的读/写(以及指令)单元正常情况下甚至都不能直接与内存进行访问,这是物理结构决定的。CPU和缓存进行通信,而缓存才能与内... 查看详情

java内存模型jmm详解!(代码片段)

...一系列的Java虚拟机平台对开发者提供的多线程环境下的内存可见性、是否可以重排序等问题的无关具体平台的统一的保证。(可能在术语上与Java运行时内存分布有歧义,后者指堆、方法区、线程栈等内存区域)。并发编程有多种... 查看详情