JVM——Java内存模型

Java内存模型

在Java多线程编程中,Java内存模型(Java Memory Model, JMM)是理解程序执行行为和实现线程安全的关键。下面我们深入探讨Java内存模型的内容。

Java内存模型概述

Java内存模型定义了Java程序中变量的内存操作规则,以及线程之间的通信语义。它屏蔽了底层硬件和操作系统的差异,为Java程序员提供了一个统一的内存访问视图。在JMM中,每个线程都有自己的工作内存,而共享变量存储在主内存中。线程对变量的所有操作都必须通过工作内存进行,不能直接读写主内存中的变量。工作内存中的变量是主内存中变量的副本,线程之间的通信必须通过主内存进行。

happens-before关系与内存可见性

(一)happens-before关系定义

happens-before关系是Java内存模型中的核心概念之一,用于描述两个操作之间的内存可见性。如果操作X happens-before操作Y,那么X的执行结果对Y是可见的。JMM通过定义一系列规则来确定两个操作之间是否存在happens-before关系:

  1. 程序顺序规则:在单线程中,按照代码的顺序,前面的操作happens-before后面的操 作。但这并不意味着指令不能重排序,只是重排序后必须保证单线程内的结果与顺序执行一致。
  2. 监视器锁规则:一个解锁操作happens-before于后续对同一把锁的加锁操作。这保证了线程释放锁后,其他线程获取锁时能看到之前线程对共享变量的修改。
  3. volatile变量规则:对一个volatile变量的写操作happens-before于后续对同一变量的读操作。这确保了volatile变量的修改对其他线程立即可见。
  4. 线程启动规则:线程的启动操作happens-before于该线程的其他操作。这使得新启动的线程能看到创建线程对共享变量的初始化操作。
  5. 线程终止规则:线程的所有操作happens-before于其他线程检测到该线程已经终止(如通过Thread.join()或Thread.isAlive()判断)。
  6. 中断规则:一个线程的中断操作happens-before于被中断线程检测到中断事件。
  7. 传递性:如果操作A happens-before操作B,操作B happens-before操作C,那么操作A happens-before操作C。

(二)内存可见性问题

在多线程环境下,由于每个线程都有自己的工作内存,线程对共享变量的修改可能无法及时同步到主内存,导致其他线程无法看到最新的值。happens-before关系通过确保特定操作的有序性,解决了内存可见性问题。例如,通过使用volatile关键字修饰共享变量,可以保证对该变量的写操作happens-before读操作,从而确保线程间对该变量的修改可见。

(三)重排序与数据竞争

编译器和处理器为了优化性能,可能会对指令进行重排序。但在多线程环境下,不合理的重排序可能导致数据竞争问题。例如,一个线程写入共享变量后,另一个线程读取该变量,但由于指令重排序,读取操作可能在写入操作之前执行,导致读取到旧值。通过建立正确的happens-before关系,可以避免重排序带来的数据竞争问题。

Java内存模型的底层实现

(一)内存屏障

Java内存模型通过内存屏障(memory barrier)来控制编译器和处理器的重排序行为。内存屏障是一组指令,用于确保特定操作的执行顺序,并强制刷新处理器缓存。常见的内存屏障类型包括:

  • LoadLoad屏障 :确保加载操作的顺序。
  • LoadStore屏障 :确保加载操作在存储操作之前完成。
  • StoreLoad屏障 :确保存储操作在加载操作之前完成。
  • StoreStore屏障 :确保存储操作的顺序。

在JMM中,不同的happens-before关系对应不同的内存屏障插入策略。例如,volatile变量的写操作前后会插入StoreLoad屏障,以防止重排序。

(二)即时编译器优化与内存屏障

即时编译器(JIT)在编译Java字节码为机器码时,会根据JMM插入相应的内存屏障。对于不同的处理器架构,内存屏障的具体实现可能不同。例如,在X86架构上,由于其对内存访问的强序一致性支持,某些内存屏障可以省略或简化。即时编译器需要根据具体的处理器特性,生成高效的机器码,同时保证JMM的语义。

(三)处理器缓存与内存一致性

现代处理器使用缓存来提高内存访问速度。每个处理器核心都有自己的缓存,这可能导致不同核心看到的内存值不一致。内存屏障通过强制刷新缓存,确保共享变量的修改对其他处理器核心可见。例如,当一个线程对共享变量进行修改后,内存屏障会将该变量的值从工作内存同步到主内存,并刷新其他处理器核心中的缓存行,使得其他线程能够读取到最新的值。

锁与同步

(一)锁的happens-before关系

锁是Java中实现线程同步的重要机制。在JMM中,锁的获取和释放操作具有特定的happens-before关系:

  • 解锁happens-before加锁 :一个线程对某个对象的解锁操作happens-before于其他线程对该对象的加锁操作。这确保了释放锁之前对该对象的修改对后续获取锁的线程可见。
  • 锁的范围内的操作顺序 :在单线程中,锁获取之后的操作happens-before于锁释放之前的操作。这保证了锁范围内的操作具有一定的顺序性。

(二)synchronized关键字的实现

synchronized关键字通过对象头中的锁标志位和Monitor来实现线程同步。当一个线程进入synchronized代码块时,它会获取对象的锁,并在退出代码块时释放锁。在JMM中,锁的获取和释放操作会插入相应的内存屏障,确保线程之间的内存可见性。

(三)锁的优化与性能

为了提高性能,JVM对锁进行了多种优化,如偏向锁、轻量级锁和重量级锁的转换。偏向锁通过记录线程ID,减少同一线程多次获取锁的开销;轻量级锁通过CAS操作实现锁的获取和释放,避免进入重量级的Monitor等待队列。这些优化措施在保证线程安全的同时,提高了程序的执行效率。

volatile字段

(一)volatile的内存语义

volatile关键字是Java中实现轻量级线程同步的重要工具。被volatile修饰的变量具有以下特性:

  • 可见性 :对volatile变量的修改对其他线程立即可见。JMM通过在volatile变量的读写操作前后插入内存屏障,确保修改操作及时同步到主内存,并刷新其他线程的工作内存。
  • 有序性 :禁止编译器和处理器对volatile变量的读写操作进行重排序。这保证了程序的执行顺序与代码的逻辑顺序一致,避免了由于重排序导致的内存可见性问题。

(二)volatile的使用场景与限制

  • 使用场景 :volatile适用于单个变量的状态标记,如表示任务完成、线程停止等布尔标志。例如,一个线程通过修改volatile布尔变量来通知其他线程任务已完成。
  • 限制 :volatile不能保证复合操作的原子性。例如,对volatile变量进行递增操作(i++)并不是原子操作,可能会出现线程安全问题。在需要保证复合操作原子性的场景下,应使用锁或其他同步机制。

(三)volatile底层实现与性能

在底层实现上,JVM通过在volatile变量的读写操作中插入内存屏障来保证内存可见性和禁止重排序。在X86架构中,volatile写操作会生成lock前缀的指令(如lock addl $0x0, (%rsp)),该指令具有内存屏障的效果,强制刷新处理器缓存。volatile读操作则通过加载主内存中的最新值来保证可见性。与锁相比,volatile的性能开销较低,但在频繁读写的情况下,由于每次都需要访问主内存,可能会导致性能瓶颈。

final字段与安全发布

(一)final字段的内存语义

final关键字修饰的字段具有特殊的内存语义:

  • 写操作的有序性 :在JMM中,final字段的写操作后会插入一个StoreStore屏障,禁止将final字段的写操作重排序到构造函数返回之前。这确保了新建对象的final字段在对象引用被其他线程获取后不会被修改,并且其他线程能够看到final字段的正确初始化值。
  • 读操作的可见性 :一旦一个对象的引用被发布(即对象引用被其他线程获取),该对象的final字段的初始化值对其他线程可见。这是因为final字段的写操作与对象引用的写操作之间存在happens-before关系。

(二)安全发布对象

安全发布(safe publication)是指确保一个对象的初始化完成,并且该对象的所有字段(包括非final字段)的值对其他线程可见。JMM提供了以下几种安全发布对象的方式:

  • 使用final关键字 :final字段的写操作与对象引用的写操作之间存在happens-before关系,确保对象的final字段在对象引用被发布后对其他线程可见。此外,JMM还保证,一旦对象的引用被发布,该对象的其他字段的值至少会看到构造函数中对这些字段的最后设置值。
  • 使用volatile关键字修饰对象引用 :将对象引用声明为volatile,可以确保对象的引用对其他线程可见,并且其他线程在读取该引用后能看到对象的最新状态。volatile对象引用的写操作happens-before读操作,保证了对象的可见性。
  • 使用同步机制 :通过锁来保护对象的引用和对象的初始化过程,确保对象的引用和状态在锁保护下对其他线程可见。例如,在构造函数中加锁,并在发布对象引用时确保锁的正确获取和释放。

(三)final字段与不可变对象

final字段常用于构建不可变对象。不可变对象一旦创建后,其状态不能被修改。通过将对象的字段声明为final,并在构造函数中完成初始化,可以确保对象的不可变性。不可变对象具有线程安全的特性,因为它们的状态不会改变,无需额外的同步措施。例如,Java中的String类就是一个典型的不可变对象,其字符数组被声明为final,确保字符串的内容在创建后不可修改。

实际案例与实践建议

(一)避免指令重排序导致的错误

在多线程环境下,指令重排序可能导致程序行为与预期不符。例如,考虑以下代码:

class UnsafePublication {int x = 1;int y = 2;public static void main(String[] args) {UnsafePublication up = new UnsafePublication();System.out.println("x: " + up.x + ", y: " + up.y);}
}

如果对象up的引用被发布前,其字段x和y的初始化操作被重排序,其他线程可能看到x和y的默认值(0)而不是初始化值。为避免此类问题,应使用安全发布机制,如将字段声明为final,或使用同步机制确保对象正确发布。

(二)使用volatile确保变量可见性

在需要频繁更新的状态标志场景下,volatile是合适的选择。例如:

public class StopThread {private volatile boolean stopRequested = false;public void run() {while (!stopRequested) {// 执行任务}}public void stop() {stopRequested = true;}
}

通过将stopRequested声明为volatile,确保run方法中的循环条件能够及时看到stop方法对变量的修改,从而安全地停止线程。

(三)利用final构建不可变对象

构建不可变对象可以简化线程安全设计。例如:

public final class ImmutableObject {private final int value;public ImmutableObject(int value) {this.value = value;}public int getValue() {return value;}
}

ImmutableObject的value字段被声明为final,确保对象创建后其值不可修改。这使得该对象可以安全地在多个线程间共享,无需额外的同步措施。

总结

Java内存模型是Java多线程编程的基石,它通过happens-before关系、内存屏障、锁、volatile和final等机制,为开发者提供了控制内存可见性和线程同步的工具。深入理解JMM的原理和实践,能够帮助开发者避免常见的并发编程错误,设计出高效、可靠的多线程应用。在实际开发中,应根据具体的场景选择合适的同步机制,如使用volatile确保变量可见性、final构建不可变对象以及锁保护共享资源等,以实现高效的线程安全。

在阅读本文以后,还可以拓展阅读我之前写的什么是 Java 内存模型?这篇文章。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

nRF Connect SDK system off模式介绍

目录 概述 1. 软硬件环境 1.1 软件开发环境 1.2 硬件环境 2 System Off 模式 2.1 模式介绍 2.2 注意事项 3 功能实现 3.1 框架结构介绍 3.2 代码介绍 4 功能验证 4.1 编译和下载代码 4.2 测试 4.3 使能CONFIG_APP_USE_RETAINED_MEM的测试 5 main.c的源代码文件…

白杨SEO:如何查看百度、抖音、微信、微博、小红书、知乎、B站、视频号、快手等7天内最热门话题及流量关键词有哪些?使用方法和免费工具推荐以及注意事项【干货】

大家好,我是白杨SEO,专注SEO十年以上,全网SEO流量实战派,AI搜索优化研究者。 (温馨提醒:本文有点长,看不完建议先收藏或星标,后面慢慢看哈) 最近,不管是在白…

2025 Mac常用软件安装配置

1、homebrew 2、jdk 1、使用brew安装jdk: brew install adoptopenjdk/openjdk/adoptopenjdk8 jdk默认安装位置在 /Library/Java/JavaVirtualMachines/adoptopenjdk-8.jdk/Contents/Home 目录。 2、配置环境变量: vim ~/.zshrc# Jdk export JAVA_HOM…

Linux 内核学习(6) --- Linux 内核基础知识

目录 Linux 内核基础知识进程调度内存管理虚拟文件系统和网络接口进程间通信Linux 内核编译Makefile 和 Kconfig内核Makefile内核Kconfig 配置项标识的写法depend 关键字select 关键字表达式逻辑关系Kconfig 其他语法 配置文件的编译Linux 内核引导方法Booloader 定义Linux 内核…

常见汇编代码及其指令

1. 数据传输指令 1.1. mov 作用:将数据从源操作数复制到目标操作数。语法:mov dest, src mov eax, 10 ; 将立即数 10 存入 eax 寄存器 mov ebx, eax ; 将 eax 的值复制到 ebx mov [ecx], eax ; 将 eax 的值写入 ecx 指向的内存地址 1.2. …

STM32基础教程——软件SPI

目录 前言 技术实现 接线图 代码实现 技术要点 引脚操作 SPI初始化 SPI起始信号 SPI终止信号 SPI字节交换 宏替换命令 W25Q64写使能 忙等待 读取设备ID号和制造商ID 页写入 数据读取 实验结果 问题记录 前言 SPI(Serial Peripheral Interf…

(B题|矿山数据处理问题)2025年第二十二届五一数学建模竞赛(五一杯/五一赛)解题思路|完整代码论文集合

我是Tina表姐,毕业于中国人民大学,对数学建模的热爱让我在这一领域深耕多年。我的建模思路已经帮助了百余位学习者和参赛者在数学建模的道路上取得了显著的进步和成就。现在,我将这份宝贵的经验和知识凝练成一份全面的解题思路与代码论文集合…

无网络环境下配置并运行 word2vec复现.py

需运行文件 # -*- coding: utf-8 -*- import torch import pandas as pd import jieba import torch import torch.nn as nn from tqdm import tqdm from torch.utils.data import DataLoader,Dataset from transformers import AutoTokenizer,AutoModeldef get_stop_word():w…

读《暗时间》有感

读《暗时间》有感 反思与笔记 这本书还是我无意中使用 ima 给我写职业规划的时候给出的,由于有收藏的习惯,我就去找了这本书。当读到第一章暗时间的时候给了我很大的冲击,我本身就是一个想快速读完一本书的人,看到东西没有深入思…

ubuntu安装Go SDK

# 下载最新版 Go 安装包(以 1.21.5 为例) wget https://golang.google.cn/dl/go1.21.5.linux-amd64.tar.gz # 解压到系统目录(需要 root 权限) sudo tar -C /usr/local -xzf go1.21.5.linux-amd64.tar.gz # 使用 Go 官方安装脚本…

FFmpeg(7.1版本)编译生成ffplay

FFmpeg在编译的时候,没有生成ffplay,怎么办? 1. 按照上一篇文章:FFmpeg(7.1版本)在Ubuntu18.04上的编译_ffmpeg-7.1-CSDN博客 在build.sh脚本里配置了ffplay 但是,实际上却没有生成ffplay,会是什么原因呢? 2. 原因是编译ffplay的时候,需要一些依赖库 sudo apt-get i…

【Python 函数】

Python 中的函数(Function)是可重复使用的代码块,用于封装特定功能并提高代码复用性。以下是函数的核心知识点: 一、基础语法 1. 定义函数 def greet(name):"""打印问候语""" # 文档字符串&…

7. HTML 表格基础

表格是网页开发中最基础也最实用的元素之一,尽管现代前端开发中表格布局已被 CSS 布局方案取代,但在展示结构化数据时,表格依然发挥着不可替代的作用。本文将基于提供的代码素材,系统讲解 HTML 表格的核心概念与实用技巧。 一、表格的基本结构 一个完整的 HTML 表格由以下…

极狐GitLab 命名空间的类型有哪些?

极狐GitLab 是 GitLab 在中国的发行版,关于中文参考文档和资料有: 极狐GitLab 中文文档极狐GitLab 中文论坛极狐GitLab 官网 命名空间 命名空间在极狐GitLab 中组织项目。因为每一个命名空间都是单独的,您可以在多个命名空间中使用相同的项…

powershell批处理——io校验

powershell批处理——io校验 在刷题时,时常回想,OJ平台是如何校验竞赛队员提交的代码的,OJ平台并不看代码,而是使用“黑盒测试”,用测试数据来验证。对于每题,都事先设定了很多组输入数据(data…

前端面经-webpack篇--定义、配置、构建流程、 Loader、Tree Shaking、懒加载与预加载、代码分割、 Plugin 机制

看完本篇你将基本了解webpack!!! 目录 一、Webpack 的作用 1、基本配置结构 2、配置项详解 1. entry —— 构建入口 2. output —— 输出配置 3. mode:模式设置 4. module:模块规则 5. plugins:插件机制 6. resolve:模块解析配置(可选) 7. devServer:开发服务器…

面试算法刷题练习1(核心+acm)

3. 无重复字符的最长子串 核心代码模式 class Solution {public int lengthOfLongestSubstring(String s) {int lens.length();int []numnew int[300];int ans0;for(int i0,j0;i<len;i){num[s.charAt(i)];while(num[s.charAt(i)]>1){num[s.charAt(j)]--;j;}ansMath.max…

拉削丝锥,螺纹类加工的选择之一

在我们的日常生活中&#xff0c;螺纹连接无处不在&#xff0c;从简单的螺丝钉到复杂的机械设备&#xff0c;都离不开螺纹的精密加工。今天&#xff0c;给大家介绍一种的螺纹刀具——拉削丝锥&#xff1a; 一、拉削丝锥的工作原理 拉削丝锥&#xff0c;听起来有点陌生吧&#…

数据清洗-电商双11美妆数据分析(二)

1.接下来用seaborn包给出每个店铺各个大类以及各个小类的销量销售额 先观察销量&#xff0c;各店小类中销量最高的是相宜本草的补水类商品以及妮维雅的清洁类商品&#xff0c;这两类销量很接近。而销售额上&#xff0c;相宜本草的补水类商品比妮维雅的清洁类商品要高得多&#…

【上位机——MFC】对话框

对话框的使用 1.添加对话框资源 2.定义一个自己的对话框类(CMyDlg)&#xff0c;管理对话框资源&#xff0c;派生自CDialog或CDialogEx均可 对话框架构 #include <afxwin.h> #include "resource.h"class CMyDlg :public CDialog {DECLARE_MESSAGE_MAP() publi…