【Java并发编程】Java内存模型

简介: 探讨Java的内存模型

Java内存模型

一、JMM解析

之前写过一篇文章【Java核心技术卷】谈谈对Java平台的理解,其中讨论“Java跨平台”的篇幅占了大半的位置,JVM的重要性不言而喻。

为了能够屏蔽各种硬件以及对操作系统的内存访问的差异,而且要能使得Java程序在各个平台下都能达到一致的并发效果。JVM规范中定义了Java的内存模型(Java Memory model, JMM)。

JMM是一种规范,它规范了JVM与计算机内存是如何协同工作的,规定了一个线程如何以及何时看到其他线程修改过的共享变量的值以及在必须时如何同步地访问到共享变量。


在这里插入图片描述
下面我们来认识认识JMM,首先看一下规范下的JVM的内存分配。Heap为堆,Stack 为栈。

堆是一个运行时的数据区,也是Java的垃圾回收器重点关注的对象。堆的优势在于可以动态分配内存的大小,缺点是因为是运行时动态分配内存,存取速度要慢一点。

栈的优势存取的速度比堆要快,但是比计算机的寄存器要慢哈,栈的数据是可以共享的,但是存在栈中的数据的大小与生存期是确定的,灵活性较低,栈中主要存放一些基本类型的变量。比如int,short,long,byte,对象句柄等。

JMM要求调用栈和本地变量存放在线程栈上,对象存放在堆上。对了,一个对象可能包含方法,方法可能包含本地变量,这些本地变量仍然是存放在线程栈上的,即使这些对象拥有这些方法,也是要把这些对象放在堆中。

一个对象的成员变量随着对象存放在堆中,无论这些成员变量是原始类型还是引用类型。静态成员变量跟随类的定义一起存放在堆上,存放在堆上的对象可以被持有这个对象的引用的线程访问。线程通过引用访问这个对象的时候,也是能够访问这个对象的成员变量。如果多个线程同时调用同一个对象同一个方法,它们可能都将访问这个对象的成员变量,这个时候每个线程都会拥有相应的成员变量的私有拷贝
在这里插入图片描述
私有拷贝,有疑惑吗?我这里演示一下吧

public class Main {
    
    public static void main(String[] args) {
        new Thread(() -> {
            Person person1 = new Person();
            person1.count();
            System.out.println(person1.getId());
        }).start();

        new Thread(() -> {
            Person person2 = new Person();
            person2.count();
            System.out.println(person2.getId());
        }).start();
    }
}

class Person {
    private Long id = 0L;
    public void count(){
        id++;
    }

    public Long getId() {
        return id;
    }
}

结果:
在这里插入图片描述


上面是在JVM层次看多线程的。

下面看看硬件内存架构

二、硬件内存架构

在这里插入图片描述
上面展示的是多个CPU,有个概念,这里需要点一下,你可千万别搞混了

多核CPU指的是一个CPU有多个CPU核心,多核CPU性能非常好,但成本较高;如果没钱,可以换为多个单核的CPU,如果有钱可以换成多个多核的CPU。

现在我们使用的计算机大都都是多个多核CPU了,这使得在实际使用的时候,会有多个CPU上都跑的有进程(线程),我们的Java程序如果是并发的,可能会在多个CPU上跑。

我们看一下CPU的寄存器,每个CPU都包含一系列的寄存器,它们是CPU内存的基础,寄存器执行的速度远大于主存上执行的速度,中间的缓存,我就不多说了,上篇文章介绍过了【Java并发编程】CPU多级缓存

CPU从主存中读取数据的时候,首先会将数据读取到缓存中,然后由缓存读取到寄存器中,然后再去执行,执行完步骤后,如果需要将结果写回到主存中,首先要将数据刷新到缓存中,缓存会在未来的某个时间点,将结果刷新到主存中。

三、JMM与硬件内存架构的关联

在这里插入图片描述

Java内存模型与硬件架构模型之间是存在一些差异的,硬件架构模型没有区分线程栈与堆。对于硬件而言,所有的线程栈与堆都分配在主内存里面,部分线程栈和堆可能会出现在CPU的缓存中和CPU内部的寄存器中。

四、Java线程与计算机主内存之间的抽象关系

线程之间的共享变量存储在主内存里面,每一个线程都有一个私有的本地内存,本地内存是Java内存模型抽象的概念,并不是真实存在的,它涵盖了缓存、寄存器以及其他的硬件和编译器的优化等,本地内存中存储了该线程已读或写,共享变量拷贝的一个副本。Java内存模型的工作内存是CPU的寄存器和高速缓存的一个抽象的描述。Java内存模型的存储划分仅是是对其内部的物理划分而已,只局限在JVM的内存。

在这里插入图片描述
由于每个线程都有自己的本地内存,它们如果同时访问主内存的共享变量,共享内存的值会分别copy到每个线程的本地变量中。每个线程对自己本地内存中的值做出的修改对其他线程都是不可见的,这个时候就会导致不一致性。

比如说主内存某个共享变量值为1,A和B线程都要对这个这个共享变量做出修改,A和B线程都先把值copy到自己的本地内存中,然后进行操作,A线程对其进行加1,并将值刷新到主内存中,B线程将其加2,但是相对于A线程慢了半拍,但是也成功将值刷新到主内存中。
此时,主内存中这个共享变量的值是3,当A再次从主内存中读取这个共享变量(中间会copy到它的本地内存),值已经不是2了。这个时候就导致了线程的安全性问题。

五、Java内存模型中同步八种操作

  1. lock(锁定):作用于主内存的变量,把—个变量标识为一条线程独占状态
  2. unlock(解锁):作用于主内存的变量,把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定
  3. read(读取):作用于主内存的变量,把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
  4. load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中
  5. use(使用):作用于工作内存的变量,把工作内存中的一个变量值传递给执行引擎
  6. assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋值给工作內存的变量
  7. store(存储):作用于工作内存的变量,把工作内存中的一个变量的值传送到主内存中,以便随后的 write的操作
  8. write(写入):作用于主内存的变量,它把 store操作从工作内存中一个变量的值传送到主内存的变量中

在这里插入图片描述

Lock作用于主内存的变量,它把一个变量标识为一个线程独占的状态,与其对应的就是unlock

Read读取,也是作用于主内存的变量,它变量的值从主内存变量输送到工作内存中(未到工作内存),与后边的load动作对接

Load是载入的意思,它将Read操作中变量的值放入工作内存的变量副本中

Use是使用,作用于工作内存中的变量, 它将工作内存中的变量传递给执行引擎,每当JVM遇到一个需要使用到的变量值的字节码指令的时候就会执行use这个操作。

Assign为赋值,作用于工作内存中的变量,它把从执行引擎接收到的值赋值给工作内存中的变量,每当JVM遇到一个需要给变量赋值的字节码指令的时候就会执行assign这个操作。

接下来是Store,也就是存储,它作用于工作内存中的变量,它将工作内存中的变量传递到主内存中(未到主内存),与后边的write操作对接

Write是写入的操作,它将Store操作中变量的值,放入到主内存的变量里面。

对应的同步规则有:

  • 如果要把一个变量从主内存中复制到工作内存,就需要按顺寻地执行read和load操作,如果把变量从工作内存中同步回主内存中,就要按顺序地执行 store和 write操作。但Java内存模型只要求上述操作必须按顺序执行,而没有保证必须是连续执行
  • 不允许read和load、 store和 write操作之一单独出现
  • 不允许一个线程丢弃它的最近 assign的操作,即变量在工作内存中改变了之后必须同步到主内存中
  • 不允许一个线程无原因地(没有发生过任何 assign操作)把数据从工作内存同步回主内存中
  • 一个新的变量只能在主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或 assign)的变量。即就是对一个变量实施use和 store操作之前,必须先执行过了 assign和load操作
  • 一个变量在同一时刻只允许一条线程对其进行lock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的 unlock操作,变量才会被解锁。lock和 unlock必须成对出现
  • 如果对一个变量执行lock操作,将会清空工作内存中此变量的值,在执行引擎使用这个变量前需要重新执行load或assign操作初始化变量的值
  • 如果一个变量事先没有被lock操作锁定,则不允许对它执行unlock操作;也不允许去 unlock—个被其他线程锁定的变量
  • 对一个变量执行uηlock操作之前,必须先把此变量同步到主内存中(执行 store和 write操作)

Java并发相关的类设计时都遵循的规则,还有一些特殊的规则,之后再说。

目录
相关文章
|
7天前
|
安全 Java 程序员
深入理解Java内存模型与并发编程####
本文旨在探讨Java内存模型(JMM)的复杂性及其对并发编程的影响,不同于传统的摘要形式,本文将以一个实际案例为引子,逐步揭示JMM的核心概念,包括原子性、可见性、有序性,以及这些特性在多线程环境下的具体表现。通过对比分析不同并发工具类的应用,如synchronized、volatile关键字、Lock接口及其实现等,本文将展示如何在实践中有效利用JMM来设计高效且安全的并发程序。最后,还将简要介绍Java 8及更高版本中引入的新特性,如StampedLock,以及它们如何进一步优化多线程编程模型。 ####
14 0
|
9天前
|
Java 程序员
Java编程中的异常处理:从基础到高级
在Java的世界中,异常处理是代码健壮性的守护神。本文将带你从异常的基本概念出发,逐步深入到高级用法,探索如何优雅地处理程序中的错误和异常情况。通过实际案例,我们将一起学习如何编写更可靠、更易于维护的Java代码。准备好了吗?让我们一起踏上这段旅程,解锁Java异常处理的秘密!
|
6天前
|
安全 算法 Java
Java多线程编程中的陷阱与最佳实践####
本文探讨了Java多线程编程中常见的陷阱,并介绍了如何通过最佳实践来避免这些问题。我们将从基础概念入手,逐步深入到具体的代码示例,帮助开发者更好地理解和应用多线程技术。无论是初学者还是有经验的开发者,都能从中获得有价值的见解和建议。 ####
|
6天前
|
Java 调度
Java中的多线程编程与并发控制
本文深入探讨了Java编程语言中多线程编程的基础知识和并发控制机制。文章首先介绍了多线程的基本概念,包括线程的定义、生命周期以及在Java中创建和管理线程的方法。接着,详细讲解了Java提供的同步机制,如synchronized关键字、wait()和notify()方法等,以及如何通过这些机制实现线程间的协调与通信。最后,本文还讨论了一些常见的并发问题,例如死锁、竞态条件等,并提供了相应的解决策略。
23 3
|
17天前
|
缓存 Prometheus 监控
Elasticsearch集群JVM调优设置合适的堆内存大小
Elasticsearch集群JVM调优设置合适的堆内存大小
150 1
|
7天前
|
存储 监控 算法
深入探索Java虚拟机(JVM)的内存管理机制
本文旨在为读者提供对Java虚拟机(JVM)内存管理机制的深入理解。通过详细解析JVM的内存结构、垃圾回收算法以及性能优化策略,本文不仅揭示了Java程序高效运行背后的原理,还为开发者提供了优化应用程序性能的实用技巧。不同于常规摘要仅概述文章大意,本文摘要将简要介绍JVM内存管理的关键点,为读者提供一个清晰的学习路线图。
|
16天前
|
Java
JVM内存参数
-Xmx[]:堆空间最大内存 -Xms[]:堆空间最小内存,一般设置成跟堆空间最大内存一样的 -Xmn[]:新生代的最大内存 -xx[use 垃圾回收器名称]:指定垃圾回收器 -xss:设置单个线程栈大小 一般设堆空间为最大可用物理地址的百分之80
|
17天前
|
Java
JVM运行时数据区(内存结构)
1)虚拟机栈:每次调用方法都会在虚拟机栈中产生一个栈帧,每个栈帧中都有方法的参数、局部变量、方法出口等信息,方法执行完毕后释放栈帧 (2)本地方法栈:为native修饰的本地方法提供的空间,在HotSpot中与虚拟机合二为一 (3)程序计数器:保存指令执行的地址,方便线程切回后能继续执行代码
18 3
|
17天前
|
存储 缓存 监控
Elasticsearch集群JVM调优堆外内存
Elasticsearch集群JVM调优堆外内存
42 1
|
27天前
|
Arthas 监控 Java
JVM进阶调优系列(9)大厂面试官:内存溢出几种?能否现场演示一下?| 面试就那点事
本文介绍了JVM内存溢出(OOM)的四种类型:堆内存、栈内存、元数据区和直接内存溢出。每种类型通过示例代码演示了如何触发OOM,并分析了其原因。文章还提供了如何使用JVM命令工具(如jmap、jhat、GCeasy、Arthas等)分析和定位内存溢出问题的方法。最后,强调了合理设置JVM参数和及时回收内存的重要性。