【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并发相关的类设计时都遵循的规则,还有一些特殊的规则,之后再说。

目录
相关文章
|
1月前
|
存储 缓存 安全
Java内存模型深度解析:从理论到实践####
【10月更文挑战第21天】 本文深入探讨了Java内存模型(JMM)的核心概念与底层机制,通过剖析其设计原理、内存可见性问题及其解决方案,结合具体代码示例,帮助读者构建对JMM的全面理解。不同于传统的摘要概述,我们将直接以故事化手法引入,让读者在轻松的情境中领略JMM的精髓。 ####
38 6
|
21天前
|
安全 Java 程序员
深入理解Java内存模型与并发编程####
本文旨在探讨Java内存模型(JMM)的复杂性及其对并发编程的影响,不同于传统的摘要形式,本文将以一个实际案例为引子,逐步揭示JMM的核心概念,包括原子性、可见性、有序性,以及这些特性在多线程环境下的具体表现。通过对比分析不同并发工具类的应用,如synchronized、volatile关键字、Lock接口及其实现等,本文将展示如何在实践中有效利用JMM来设计高效且安全的并发程序。最后,还将简要介绍Java 8及更高版本中引入的新特性,如StampedLock,以及它们如何进一步优化多线程编程模型。 ####
24 0
|
1月前
|
存储 算法 Java
Java内存管理深度剖析与优化策略####
本文深入探讨了Java虚拟机(JVM)的内存管理机制,重点分析了堆内存的分配策略、垃圾回收算法以及如何通过调优提升应用性能。通过案例驱动的方式,揭示了常见内存泄漏的根源与解决策略,旨在为开发者提供实用的内存管理技巧,确保应用程序既高效又稳定地运行。 ####
|
23天前
|
存储 监控 算法
Java内存管理深度剖析:从垃圾收集到内存泄漏的全面指南####
本文深入探讨了Java虚拟机(JVM)中的内存管理机制,特别是垃圾收集(GC)的工作原理及其调优策略。不同于传统的摘要概述,本文将通过实际案例分析,揭示内存泄漏的根源与预防措施,为开发者提供实战中的优化建议,旨在帮助读者构建高效、稳定的Java应用。 ####
37 8
|
21天前
|
存储 监控 算法
深入探索Java虚拟机(JVM)的内存管理机制
本文旨在为读者提供对Java虚拟机(JVM)内存管理机制的深入理解。通过详细解析JVM的内存结构、垃圾回收算法以及性能优化策略,本文不仅揭示了Java程序高效运行背后的原理,还为开发者提供了优化应用程序性能的实用技巧。不同于常规摘要仅概述文章大意,本文摘要将简要介绍JVM内存管理的关键点,为读者提供一个清晰的学习路线图。
|
25天前
|
存储 算法 Java
Java 内存管理与优化:掌控堆与栈,雕琢高效代码
Java内存管理与优化是提升程序性能的关键。掌握堆与栈的运作机制,学习如何有效管理内存资源,雕琢出更加高效的代码,是每个Java开发者必备的技能。
49 5
|
26天前
|
缓存 Java 开发者
Java多线程并发编程:同步机制与实践应用
本文深入探讨Java多线程中的同步机制,分析了多线程并发带来的数据不一致等问题,详细介绍了`synchronized`关键字、`ReentrantLock`显式锁及`ReentrantReadWriteLock`读写锁的应用,结合代码示例展示了如何有效解决竞态条件,提升程序性能与稳定性。
109 6
|
23天前
|
存储 算法 Java
Java内存管理深度解析####
本文深入探讨了Java虚拟机(JVM)中的内存分配与垃圾回收机制,揭示了其高效管理内存的奥秘。文章首先概述了JVM内存模型,随后详细阐述了堆、栈、方法区等关键区域的作用及管理策略。在垃圾回收部分,重点介绍了标记-清除、复制算法、标记-整理等多种回收算法的工作原理及其适用场景,并通过实际案例分析了不同GC策略对应用性能的影响。对于开发者而言,理解这些原理有助于编写出更加高效、稳定的Java应用程序。 ####
|
23天前
|
安全 Java 程序员
Java内存模型的深入理解与实践
本文旨在深入探讨Java内存模型(JMM)的核心概念,包括原子性、可见性和有序性,并通过实例代码分析这些特性在实际编程中的应用。我们将从理论到实践,逐步揭示JMM在多线程编程中的重要性和复杂性,帮助读者构建更加健壮的并发程序。
|
28天前
|
算法 Java 开发者
Java内存管理与垃圾回收机制深度剖析####
本文深入探讨了Java虚拟机(JVM)的内存管理机制,特别是其垃圾回收机制的工作原理、算法及实践优化策略。不同于传统的摘要概述,本文将以一个虚拟的“城市环卫系统”为比喻,生动形象地揭示Java内存管理的奥秘,旨在帮助开发者更好地理解并调优Java应用的性能。 ####