JVM之Java内存模型

Java内存模型(Java Memory Model,简称JMM)是Java虚拟机(JVM)规范中定义的一套规则,用于描述多线程环境下变量如何被访问和同步。在多线程编程中,内存模型的重要性不言而喻,它直接关系到程序的正确性和性能。本文将深入探讨Java内存模型的原理、特性、示例以及总结。

一、基本概念

1. 主内存与工作内存

  • 主内存:主内存是JVM中所有线程共享的内存区域,用于存储Java程序中的变量和对象实例等数据。所有的共享变量都存储在主内存中,包括实例字段、静态字段和数组元素等。
  • 工作内存:每个线程都有自己的工作内存,也称为本地内存。工作内存是线程私有的,用于存储主内存中共享变量的副本。线程对变量的所有操作(读取、写入)都在工作内存中进行,而不是直接操作主内存。

2. 内存交互操作

JMM定义了以下八种操作来完成主内存和工作内存之间的交互:

  • lock(锁定):作用于主内存的变量,将变量标识为一条线程独占状态。
  • unlock(解锁):作用于主内存的变量,释放处于锁定状态的变量,允许其他线程锁定该变量。
  • read(读取):作用于主内存的变量,将变量值从主内存传输到线程的工作内存中。
  • load(载入):作用于工作内存的变量,将read操作从主内存中得到的变量值放入工作内存的变量副本中。
  • use(使用):作用于工作内存的变量,将工作内存中的一个变量值传递给执行引擎。
  • assign(赋值):作用于工作内存的变量,将执行引擎接收到的值赋给工作内存的变量。
  • store(存储):作用于工作内存的变量,将工作内存中的一个变量的值传送到主内存中。
  • write(写入):作用于主内存的变量,将store操作从工作内存中得到的变量的值放入主内存的变量中。

三、特性

1. 原子性(Atomicity)

  • 定义:一个或多个操作,要么全部执行,要么全部不执行,在执行过程中不会被任何因素打断。
  • 实现方式:在Java中,对基本数据类型的访问和操作是原子的,例如对int、long、short、byte、char、boolean和reference类型的读写操作都是原子的。对于复合操作(如i++),则需要通过同步机制(如synchronized、Lock等)来保证原子性。

2. 可见性(Visibility)

  • 定义:一个线程对共享变量的修改,能够被其他线程看到。

  • 实现方式:Java内存模型通过volatile关键字和synchronized关键字来实现可见性。

    • volatile关键字:当一个变量被声明为volatile时,对该变量的读写操作都会直接作用于主内存,从而保证了可见性。但是,volatile关键字不能保证操作的原子性。
    • synchronized关键字:synchronized关键字通过锁定机制来确保只有一个线程可以执行一段代码。当一个线程访问一个对象的synchronized方法或代码块时,它将获得该对象的锁,其他线程则必须等待。在解锁之前对变量的修改对其他线程是可见的。

3. 有序性(Ordering)

  • 定义:程序执行的顺序按照代码的先后顺序执行。
  • 实现方式:Java内存模型通过Happens-Before原则来定义操作之间的偏序关系,从而允许一定程度的重排序,但同时又保证程序最终执行的结果与预期一致。

三、Happens-Before原则

Happens-Before原则是Java内存模型中定义的一组偏序关系,用于判断两个操作之间的内存可见性和有序性。它包括以下几种情况:

* 程序次序规则

一个线程中的每个操作,Happens-Before于该线程中的任意后续操作。

* 监视器锁规则

对一个锁的解锁,Happens-Before于随后对这个锁的加锁。

* volatile变量规则

对一个volatile变量的写,Happens-Before于任意后续对这个volatile变量的读。

* 传递性

如果A Happens-Before B,且B Happens-Before C,那么A Happens-Before C。

* 线程启动规则

Thread对象的start()方法调用Happens-Before于该线程的每一个动作。

* 线程终止规则

线程的所有操作都Happens-Before于其他线程检测到这个线程已经终止。

* 线程中断规则

对线程interrupt()方法的调用Happens-Before于被中断线程的代码检测到中断事件的发生。

* 对象终结规则

一个对象的初始化完成(构造函数执行结束)Happens-Before于它的finalize()方法的开始。

四、代码示例

1. 可见性示例

代码示例

public class VisibilityExample {
    private static boolean ready;
    private static int number;

    public static void main(String[] args) throws InterruptedException {
        Thread one = new Thread(() -> {
            number = 42;
            ready = true; // ①
        });

        Thread two = new Thread(() -> {
            while (!ready) {
                Thread.onSpinWait(); // ②
            }
            System.out.println(number); // ③
        });

        one.start();
        two.start();
        one.join();
    }
}

问题分析

  • 在这个例子中,线程one设置了number的值并标记ready为true。线程two在一个循环中等待ready变为true,然后打印number的值。
  • 如果没有正确的同步机制,线程two可能看不到线程one对number和ready的修改,因为这两个线程的工作在不同的内存区域中。这可能导致线程two打印出number的初始值(0),而不是线程one设置的值(42)。

解决方案

  • 可以使用volatile关键字或synchronized关键字来确保内存可见性。在这个例子中,如果将ready声明为volatile,则线程one对ready的修改将立即对线程two可见,从而确保线程two能够正确打印出number的值。

修改后的代码

public class VisibilityExample {
    private static volatile boolean ready; // 使用volatile关键字
    private static int number;

    public static void main(String[] args) throws InterruptedException {
        Thread one = new Thread(() -> {
            number = 42;
            ready = true; // ①
        });

        Thread two = new Thread(() -> {
            while (!ready) {
                Thread.onSpinWait(); // ②
            }
            System.out.println(number); // ③
        });

        one.start();
        two.start();
        one.join();
    }
}

2. 有序性示例

代码示例

public class Singleton {
    private static Singleton instance;

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

问题分析

  • 在这个例子中,Singleton类使用了双重检查锁定(Double-Checked Locking)模式来确保单例模式的安全性。

  • 然而,由于指令重排序的存在,这种实现方式在某些情况下可能会出现问题。具体来说,new Singleton()这个操作实际上会有以下三个步骤:

    1. 分配一块内存M。
    2. 在内存M上初始化Singleton对象。
    3. 将M的地址赋值给instance变量。
  • 如果发生指令重排序,顺序可能会变为:

    1. 分配一块内存M。
    2. 将M的地址赋值给instance变量。
    3. 在内存M上初始化Singleton对象。
  • 在这种情况下,如果线程A在执行到步骤2时发生线程切换,切换到线程B。线程B在执行getInstance()方法时,会发现instance不为null,因此直接返回instance。然而,此时的instance对象可能还没有被初始化,如果此时访问instance的成员变量就可能触发空指针异常。

解决方案

  • 可以通过在对象引用前添加volatile关键字来解决这个问题。volatile关键字可以禁止指令重排序,确保对象在初始化完成后再将引用赋值给变量。

修改后的代码

public class Singleton {
    private static volatile Singleton instance;

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

五、内存模型与硬件内存架构的关系

Java内存模型并不是对硬件内存架构的简单模拟,而是对其的一种抽象和简化。现代计算机为了高效运行,在CPU与内存之间设置了高速缓存(Cache)作为数据缓冲,这一机制在多线程环境下可能会引发缓存一致性问题。

为了解决这个问题,处理器需要遵循一些缓存一致性协议,如MSI、MESI(Modified, Exclusive, Shared, Invalid)等。这些协议确保了多个处理器核心在访问共享内存时,能够保持数据的一致性。例如,在MESI协议中,缓存行(Cache Line)有四种状态:修改(Modified)、独占(Exclusive)、共享(Shared)和无效(Invalid)。通过这些状态,处理器可以跟踪缓存行的数据是否与其他核心保持一致。

Java内存模型(JMM)在设计时,考虑了这些硬件层面的缓存一致性问题。JMM定义了一套规范,用于描述变量在内存中的存储方式、线程如何读取和写入内存中的变量,以及这些操作如何与其他线程进行同步。JMM的核心目标是确保在多线程环境中,程序的执行结果具有可预测性,即程序的行为与单线程环境下的行为一致(在遵守JMM规则的前提下)。

为了实现这一目标,JMM引入了几个关键概念:

1. 主内存与工作内存

JMM将内存分为两部分:主内存(Main Memory)和工作内存(Working Memory)。主内存是所有线程共享的,它存储了程序中的所有变量。每个线程都有自己的工作内存,它是线程私有的,存储了线程从主内存中读取的变量的副本。线程对变量的所有读写操作都必须在工作内存中进行,不能直接操作主内存中的变量。

2. volatile关键字

volatile是Java中的一个关键字,用于修饰变量。被volatile修饰的变量具有可见性和有序性两个特性。可见性意味着当一个线程修改了volatile变量的值,其他线程会立即看到这个修改。有序性则限制了编译器和处理器对volatile变量相关操作的重排序,从而保证了程序执行的正确性。

3. 原子性、可见性和有序性

原子性是指操作是不可中断的,要么全部执行完,要么完全不执行。可见性是指一个线程对共享变量的修改对其他线程是可见的。有序性是指程序执行的顺序是按照代码的顺序来的(在遵守JMM规则的前提下)。JMM通过一系列规则来保证这三个特性的实现。

4. 先行发生原则(Happens-Before)

这是JMM定义的一种偏序关系,用于描述两个操作之间的执行顺序。如果操作A先行发生于操作B,那么操作A对共享变量的影响在操作B中是可见的。先行发生原则是判断数据是否存在竞争、线程是否安全的主要依据。

总结

Java内存模型(JMM)是JVM规范中定义的多线程环境下变量访问和同步的规则。它抽象了硬件内存架构,分为主内存和工作内存,线程通过工作内存与主内存交互。JMM保证了原子性、可见性和有序性,其中volatile和synchronized是关键实现方式。Happens-Before原则定义了操作间的偏序关系,确保内存可见性和有序性。示例展示了可见性和有序性问题及解决方案。JMM还考虑了硬件缓存一致性问题,通过规范确保多线程环境下程序执行的可预测性。

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

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

深入了解 ES6 Map:用法与实践

🤍 前端开发工程师、技术日更博主、已过CET6 🍨 阿珊和她的猫_CSDN博客专家、23年度博客之星前端领域TOP1 🕠 牛客高级专题作者、打造专栏《前端面试必备》 、《2024面试高频手撕题》 🍚 蓝桥云课签约作者、上架课程《Vue.js 和 E…

大润发易主,被阿里割肉卖了

文丨白念云 零售行业2025年伊始便迎来一则重磅消息:大润发被卖了。 1月1日晚,阿里巴巴集团发布公告,宣布子公司及NewRetail与德弘资本达成交易,以最高约131.38亿港元出售所持高鑫零售(大润发母公司)全部股…

VulnHub—potato-suncs

使用命令扫描靶机ip arp-scan -l 尝试访问一下ip 发现一个大土豆没什么用 尝试扫描一下子域名 没有发现什么有用的信息 尝试扫描端口 namp -A 192.168.19.137 -p- 尝试访问一下端口,发现都访问不进去 查看源代码发现了网页的标题 potato,就想着爆破一下密码 hydr…

docker学习记录:commit,制作自己的镜像

1.清除所有 ktkt-SYS-4028GR-TR2:~$ sudo docker rm -f $(sudo docker ps -aq)2.再操作一次tomcat,修改好,再打成一外镜像 ktkt-SYS-4028GR-TR2:~$ sudo docker images REPOSITORY TAG IMAGE ID CREATED SIZE tomcat 9.0 3…

macos安装java8

下载 dmg方式安装 安装 双击pkg运行 输入java -version验证 配置环境变量 cd ~ ls -a输入 ls -a后查看是否已经存在.bash_profile文件,如果已经存在就不需要创建,如果不存在,继续执行下方命令创建文件 touch .bash_profile /usr/l…

【每日学点鸿蒙知识】自定义键盘光标、Cavas绘制、XComponent触发键盘抬起等

【每日学点鸿蒙知识】24.08.25 【每日学点鸿蒙知识】自定义键盘光标、Cavas绘制、XComponent触发键盘抬起等 1、基于自定义键盘如何设置光标位置? 可以参考如下代码: class MyKeyboardController {public onInputChanged?: (value: string) > vo…

在Mysql环境下对数据进行增删改查

一、插入数据: insert into 表名 [(字段名)] values (字段对应的值1,字段对应的值2,…)[,(字段对应的值1,字段对应的值2,…)]; insert into students (id,name,age,height,gender,cls_id,is_delete) values (0,小明,18,180.00,2,1,0)在学生表中插入“小明”数据的…

Web网页制作之JavaScript的应用

---------------📡🔍K学啦 更多学习资料📕 免费获取--------------- 实现的功能:1.通过登录界面跳转至主页面,用户名统一为“admin”,密码统一为“admin123”,密码可显示或隐藏,输入…

Markdown编辑器——Typora(Picgo+Github图床)

Markdown编辑器——Typora(PicgoGithub图床) 文章目录 Markdown编辑器——Typora(PicgoGithub图床)安装Typora安装PicGoPicGo软件下载PicGo的npm版本下载 GitHub图床配置PicGo配置PicGo的软件配置PicGo的npm版本信息配置 配置Typo…

Unity 3D游戏开发从入门进阶到高级

本文精心整理了Unity3D游戏开发相关的学习资料,涵盖入门、进阶、性能优化、面试和书籍等多个维度,旨在为Unity开发者提供全方位、高含金量的学习指南.欢迎收藏。 学习社区 Unity3D开发者 这是一个专注于Unity引擎的开发者社区,汇聚了众多Un…

Python 21:Debug

1. Debug的作用 当程序的预期结果和实际结果不一致时,可以用Debug模式进行调试来定位问题的位置。 2. Debug使用 1)设置断点 点击行号,出现”断点“ 2)执行Debug 点击Debug 或者右键,点击debug进入debug模式 3.Debu…

(CICD)自动化构建打包、部署(Jenkins + maven+ gitlab+tomcat)

一、平滑发布与灰度发布 **什么叫平滑:**在发布的过程中不影响用户的使用,系统不会因发布而暂停对外服务,不会造成用户短暂性无法访问; **什么叫灰度:**发布后让部分用户使用新版本,其它用户使用旧版本&am…

强化学习入门谈

之前我们见识到很多机器学习大展手脚的任务场景了,但是机器学习依旧有很多软肋。 回忆一下,我们之前做的机器学习(深度学习)策略基本都是类似于"supervised learning"的方法,比如你想用CNN实现一个classifi…

colnames看似简单,却能优化数据处理流程

引言 在数据处理和分析中,变量名称是至关重要的,它们决定了数据的可读性和操作的简便性。在R语言中,colnames 函数以其简单的语法设计,提供了高效管理数据框列名的能力,尤其是在复杂的爬虫任务中显得尤为重要。本篇文…

【分布式】Hadoop完全分布式的搭建(零基础)

Hadoop完全分布式的搭建 环境准备: (1)VMware Workstation Pro17(其他也可) (2)Centos7 (3)FinalShell (一)模型机配置 0****)安…

ArcGIS中怎么把数据提取到指定范围(裁剪、掩膜提取)

最近,经常能收到怎么把数据提取到指定范围、栅格数据怎么裁剪、矢量数据怎么裁剪、栅格数据怎么掩膜提取的咨询。 下面是我对这个问题的解决思路: 对于矢量数据: ①首先把数据加载进来 ②软件界面上面的工具栏找到→地理处理→裁剪&#x…

intra-mart环境搭建笔记

一、前言 最近在做intra-mart项目,网上这些笔记比较少,在此做一下笔记。 intra-mart是由日本intra-mart公司开发和销售的工作流平台,国内确实不怎么用,日本企业用的多些,面试时会问有没有intra-mart经验。 这个自学…

智能型电瓶车充电桩在老居民区充电站中的应用优势

摘要 随着电瓶车数量的快速增长,小区内的电瓶车充电需求日益增加,但传统充电方式存在诸多安全隐患。电瓶车智能充电桩作为一种新型充电解决方案,能够有效解决充电难题,并提升充电安全性和便捷性。本文以ACX10A型电瓶车充电桩为…

生产看板真的有用吗?

​看板,对于从事制造行业的人员来说,这并不陌生。但是对于看板起到的作用,那可就是众说纷纭,有人说,看板是领导的“面子工程”,是混淆上级视察的工具;也有人说,看板真切地帮助车间提…

刷服务器固件

猫眼淘票票 大麦 一 H3C通用IP 注:算力服务器不需要存储 二 刷服务器固件 1 登录固定IP地址 2 升级BMC版本 注 虽然IP不一致但是步骤是一致的 3 此时服务器会出现断网现象,若不断网等上三分钟ping一下 4 重新登录 5 断电拔电源线重新登录查看是否登录成功