设计模式3:单例模式:JMM与volatile和synchronized的关系
本文目录
- JMM简介
- Java 内部内存模型(The Internal Java Memory Model)
- 硬件内存架构(Hardware Memory Architecture)
- 弥合 Java 内存模型和硬件内存架构之间的差距(Bridging The Gap Between The Java Memory Model And The Hardware Memory Architecture)
- 1.共享对象的可见性
- 2.竞争条件
JMM简介
Java 内存模型指定 Java 虚拟机如何使用计算机内存 (RAM)。 Java 虚拟机是整个计算机的模型,因此该模型自然包括内存模型 - 又称为 Java 内存模型。
如果您想设计行为正确的并发程序,那么了解 Java 内存模型非常重要。 Java内存模型指定不同线程如何以及何时可以看到其他线程写入共享变量的值,以及如何在必要时同步对共享变量的访问。
原来的Java内存模型是不够的,所以在Java 1.5中对Java内存模型进行了修改。 这个版本的 Java 内存模型至今仍在 Java 中使用(Java 14+)。
Java 内部内存模型(The Internal Java Memory Model)
JVM 内部使用的 Java 内存模型在线程栈和堆之间划分内存。 下图从逻辑角度说明了Java内存模型:
Java虚拟机中运行的每个线程都有自己的线程栈。 线程栈包含有关线程调用了哪些方法来到达当前执行点的信息。 我将其称为“调用栈”(call stack)。 当线程执行其代码时,调用栈会发生变化。
线程栈还包含正在执行的每个方法的所有局部变量(调用栈上的所有方法)。 线程只能访问它自己的线程栈。 线程创建的局部变量对于除创建它的线程之外的所有其他线程都是不可见的。 即使两个线程正在执行完全相同的代码,这两个线程仍然会在各自的线程栈中创建该代码的局部变量。 因此,每个线程都有自己的每个局部变量的版本。
所有基本类型的局部变量(boolean、byte、short、char、int、long、float、double)都完全存储在线程栈上,因此对其他线程不可见。 一个线程可以将原始变量的副本传递给另一个线程,但它不能共享原始局部变量本身。
堆包含在 Java 应用程序中创建的所有对象,无论哪个线程创建了该对象。 这包括基本类型的对象版本(例如 Byte、Integer、Long 等)。 无论一个对象是被创建并分配给局部变量,还是作为另一个对象的成员变量创建,该对象仍然存储在堆上。
下面的图表说明了存储在线程栈上的调用堆栈和局部变量以及存储在堆上的对象:
Java 内存模型显示局部变量和对象在内存中的存储位置。
局部变量可能是原始类型,在这种情况下,它完全保存在线程栈上。
局部变量也可以是对对象的引用。 在这种情况下,引用(局部变量)存储在线程栈上,但对象本身存储在堆上。
对象可以包含方法,并且这些方法可以包含局部变量。 这些局部变量也存储在线程栈上,即使方法所属的对象存储在堆上。
对象的成员变量与对象本身一起存储在堆上。 当成员变量是基本类型以及对对象的引用时都是如此。
静态类变量也与类定义一起存储在堆上。
堆上的对象可以被所有引用该对象的线程访问。 当线程可以访问对象时,它也可以访问该对象的成员变量。 如果两个线程同时调用同一个对象的方法,它们都可以访问该对象的成员变量,但每个线程都有自己的局部变量副本。
这是说明上述几点的图表:
Java 内存模型显示从局部变量到对象以及从对象到其他对象的引用。
两个线程有一组局部变量。 局部变量之一(局部变量 2)指向堆上的共享对象(对象 3)。 这两个线程各自对同一对象有不同的引用。 它们的引用是局部变量,因此存储在每个线程的线程栈中(每个线程)。 不过,这两个不同的引用指向堆上的同一个对象。
请注意共享对象(对象 3)如何将对象 2 和对象 4 作为成员变量进行引用(如从对象 3 到对象 2 和对象 4 的箭头所示)。 通过对象 3 中的这些成员变量引用,两个线程可以访问对象 2 和对象 4。
该图还显示了一个指向堆上两个不同对象的局部变量。 在本例中,引用指向两个不同的对象(对象 1 和对象 5),而不是同一个对象。 理论上,如果两个线程都引用了对象 1 和对象 5,则两个线程都可以访问这两个对象。 但在上图中,每个线程仅具有对两个对象之一的引用。
那么,什么样的 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 {//指向 MySharedObject 单例的静态变量public static final MySharedObject sharedInstance = new MySharedObject();//指向堆上两个对象的成员变量public Integer object2 = new Integer(22);public Integer object4 = new Integer(44);public long member1 = 12345;public long member2 = 67890;
}
如果两个线程正在执行 run() 方法,则结果将是前面显示的图表。 run() 方法调用 methodOne(),methodOne() 调用 methodTwo()。
methodOne() 声明一个原始局部变量(int 类型的 localVariable1)和一个对象引用的局部变量(localVariable2)。
每个执行 methodOne() 的线程都会在各自的线程栈上创建自己的 localVariable1 和 localVariable2 副本。 localVariable1 变量将彼此完全分离,仅存在于每个线程的线程栈上。 一个线程无法看到另一个线程对其 localVariable1 副本所做的更改。
每个执行 methodOne() 的线程也将创建自己的 localVariable2 副本。 然而,localVariable2 的两个不同副本最终都指向堆上的同一个对象。 该代码将 localVariable2 设置为指向静态变量引用的对象。 静态变量只有一份副本,并且该副本存储在堆上。 因此,localVariable2 的两个副本最终都指向静态变量所指向的 MySharedObject 的同一个实例。 MySharedObject 实例也存储在堆上。 它对应于上图中的对象3。
请注意 MySharedObject 类也包含两个成员变量。 成员变量本身与对象一起存储在堆上。 这两个成员变量指向另外两个 Integer 对象。 这些 Integer 对象对应于上图中的对象 2 和对象 4。
另请注意 methodTwo() 如何创建名为 localVariable1 的局部变量。 该局部变量是对 Integer 对象的对象引用。 该方法将 localVariable1 引用设置为指向新的 Integer 实例。 localVariable1 引用将存储在每个执行 methodTwo() 的线程的一份副本中。 实例化的两个 Integer 对象将存储在堆上,但由于该方法每次执行时都会创建一个新的 Integer 对象,因此执行该方法的两个线程将创建单独的 Integer 实例。 methodTwo() 内创建的 Integer 对象对应于上图中的对象 1 和对象 5。
还要注意 MySharedObject 类中 long 类型的两个成员变量,它是原始类型。 由于这些变量是成员变量,因此它们仍然与对象一起存储在堆上。 只有局部变量存储在线程栈中。
硬件内存架构(Hardware Memory Architecture)
现代硬件内存架构与内部 Java 内存模型有些不同。 了解硬件内存架构以及 Java 内存模型如何与其配合也很重要。 本节介绍常见的硬件内存架构,后面的部分将介绍 Java 内存模型如何与其配合使用。
这是现代计算机硬件架构的简化图:
现代计算机通常有 2 个或更多 CPU。 其中一些 CPU 也可能具有多个内核。 关键是,在一台具有 2 个或更多 CPU 的现代计算机上,可以有多个线程同时运行。 每个 CPU 都能够在任何给定时间运行一个线程。 这意味着,如果您的 Java 应用程序是多线程的,则每个 CPU 一个线程可能会在您的 Java 应用程序中同时运行。
每个 CPU 都包含一组寄存器,这些寄存器本质上是 CPU 内存。 CPU 在这些寄存器上执行操作的速度比在主内存中的变量上执行操作的速度快得多。 这是因为 CPU 访问这些寄存器的速度比访问主内存的速度快得多。
每个CPU还可以具有CPU高速缓存存储器层。 事实上,大多数现代 CPU 都有一定大小的高速缓存层。 CPU 访问其高速缓存的速度比主内存快得多,但通常不如访问其内部寄存器的速度快。 因此,CPU 高速缓存的速度介于内部寄存器和主存储器之间。 某些 CPU 可能有多个缓存层(1 级和 2 级),但这对于理解 Java 内存模型如何与内存交互并不那么重要。 重要的是要知道 CPU 可以有某种缓存层。
计算机还包含主存储区域(RAM)。 所有CPU都可以访问主存。 主存储器区域通常比 CPU 的高速缓冲存储器大得多。
通常,当 CPU 需要访问主存时,它会将主存的一部分读入 CPU 缓存。 它甚至可以将部分缓存读入其内部寄存器,然后对其执行操作。 当CPU需要将结果写回到主存时,它会将内部寄存器中的值刷新到高速缓冲存储器,并在某个时刻将值刷新回主存。
当CPU需要在高速缓冲存储器中存储其他内容时,存储在高速缓冲存储器中的值通常被刷新回主存储器。 CPU 缓存可以一次将数据写入其内存的一部分,并一次刷新其内存的一部分。 每次更新时不必读取/写入完整缓存。 通常,缓存在称为“缓存行”的较小内存块中更新。 一个或多个高速缓存行可被读入高速缓冲存储器,并且一个或多个高速缓存行可被再次刷新回主存储器。
弥合 Java 内存模型和硬件内存架构之间的差距(Bridging The Gap Between The Java Memory Model And The Hardware Memory Architecture)
正如已经提到的,Java 内存模型和硬件内存架构是不同的。 硬件内存架构不区分线程栈和堆。 在硬件上,线程栈和堆都位于主存中。 部分线程栈和堆有时可能存在于 CPU 缓存和内部 CPU 寄存器中。 下图对此进行了说明:
线程栈和堆在CPU内部寄存器、CPU缓存和主存之间的划分:
当对象和变量可以存储在计算机中的各种不同的存储区域中时,可能会出现某些问题。 两个主要问题是:
- 线程更新(写入)共享变量的可见性。
- 读取、检查和写入共享变量时的竞争条件。
这两个问题都将在以下部分中进行解释。
1.共享对象的可见性
如果两个或多个线程共享一个对象,而没有正确使用 易失性声明(volatile declarations )或同步(synchronization),则一个线程对共享对象所做的更新可能对其他线程不可见。
想象一下共享对象最初存储在主内存中。 然后,在 CPU 1 上运行的线程将共享对象读取到其 CPU 缓存中。 在那里它对共享对象进行更改。 只要 CPU 缓存尚未刷新回主内存,共享对象的更改版本对于其他 CPU 上运行的线程来说是不可见的。 这样,每个线程最终可能会得到自己的共享对象副本,每个副本位于不同的 CPU 缓存中。
下图说明了所描绘的情况。 运行在左侧 CPU 上的一个线程将共享对象复制到其 CPU 缓存中,并将其 count 变量更改为 2。这一更改对于运行在右侧 CPU 上的其他线程不可见,因为对 count 的更新尚未刷新回主存。
为了解决这个问题可以使用Java的 volatile 关键字。 volatile 关键字可以确保直接从主存读取给定变量,并在更新时始终写回主存。
2.竞争条件
如果两个或多个线程共享一个对象,并且多个线程更新该共享对象中的变量,则可能会出现竞争条件。
想象一下,如果线程 A 将共享对象的变量计数读取到其 CPU 缓存中。 也想象一下,线程 B 执行相同的操作,但进入不同的 CPU 缓存。 现在线程 A 将计数加一,线程 B 也执行同样的操作。 现在 var1 已增加两次,每个 CPU 缓存中一次。
如果这些增量是按顺序执行的,则变量计数将增加两次,并将原始值 + 2 写回主存储器。
然而,这两个增量是同时进行的,没有适当的同步。 无论线程 A 和 B 中哪一个将其更新版本的 count 写回主内存,尽管有两次增量,更新后的值只会比原始值高 1。
该图说明了如上所述的竞争条件问题的发生:

为了解决这个问题,你可以使用Java同步块(Java synchronized block)。 同步块保证在任何给定时间只有一个线程可以进入代码的给定关键部分。 同步块还保证在同步块内访问的所有变量都将从主内存中读入,并且当线程退出同步块时,所有更新的变量将再次刷新回主内存,无论该变量是否被声明为 volatile 或 没有声明。
相关文章:

设计模式3:单例模式:JMM与volatile和synchronized的关系
本文目录 JMM简介Java 内部内存模型(The Internal Java Memory Model)硬件内存架构(Hardware Memory Architecture)弥合 Java 内存模型和硬件内存架构之间的差距(Bridging The Gap Between The Java Memory Model And The Hardware Memory Architecture)1.共享对象的可见性2.竞…...
一个简单的OPC UA/ModbusTCP 网关(Python)
使用我前面几篇博文的内容,能够使用Python编写一个最简单的OPC UA /ModbusTCP网关。 从这个程序可以看出: 应用OPC UA 并不难,现在我们就可以应用到工程应用中,甚至DIY项目也可以。不必采用复杂的工具软件。使用Python 来构建工…...

线性代数行列式的几何含义
行列式可以看做是一系列列向量的排列,并且每个列向量的分量可以理解为其对应标准正交基下的坐标。 行列式有非常直观的几何意义,例如: 二维行列式按列向量排列依次是 a \mathbf{a} a和 b \mathbf{b} b,可以表示 a \mathbf{a} a和…...

python用flask将视频显示在网页上
注意我们的return返回值必须是以下之一,否则会报错 from flask import Flask, render_template, Response import cv2app Flask(__name__)app.route(/) def index():return render_template(index.html)def gen(camera):while True:success, image camera.read(…...

【数据挖掘】时间序列教程【一】
第一章 说明 对于时间序列的研究,可以追溯到19世纪末和20世纪初。当时,许多学者开始对时间相关的经济和社会现象进行研究,尝试发现其规律和趋势。其中最早的时间序列研究可以追溯到法国经济学家易贝尔(Maurice Allais)…...
优化索引粒度参数提升ClickHouse查询性能
当对高基数列进行过滤查询时,总是希望尽可能跳过更多的行。否则需要处理更多数据、需要更多资源。ClickHouse缺省在MergeTree表读取8192行数据块,但我们可以在创建表时调整该index_granularity 参数。本文通过示例说明如何调整该参数优化查询性能。 inde…...

selenium\webdriver\remote\errorhandler.py:242: SessionNotCreatedException问题解决
报错信息: raise exception_class(message, screen, stacktrace) E selenium.common.exceptions.SessionNotCreatedException: Message: session not created: This version of ChromeDriver only supports Chrome version 112 E Current browser versi…...

MySQL 备份与恢复
MySQL 备份与恢复 一、数据库备份的分类1.1 数据备份的重要性1.2 数据库备份的分类1.2.1 从物理与逻辑的角度,分为物理备份和逻辑备份1.2.2 从数据库的备份策略角度,分为完全备份,差异备份和增量备份1.2.3 常见的备份方法 二、MySQL完全备份与…...

js中改变this指向的三种方式
js中改变this指向的三种方式 1、call方法2、apply方法3、bind方法 1、call方法 使用 call 方法调用函数,同时指定函数中 this 的值,使用方法如下代码所示: <script>const obj {uname: 刘德华}function fn(x, y) {console.log(this) …...
小程序中如何进行数据传递和通信
103. 小程序中如何进行数据传递和通信? 1. 使用页面参数传递数据: 在小程序中,可以通过页面参数来传递数据。当跳转到一个新页面时,可以将需要传递的数据作为参数传入,然后在目标页面的onLoad函数中获取参数。 示例…...

Vue3项目中引入ElementUI使用详解
目录 Vue3项目中引入 ElementUI1.安装2.引入2.1 全局引入2.2 按需引入viteWebpack 3.使用 Vue3项目中引入 ElementUI ElementUI是一个强大的PC端UI组件框架,它不依赖于vue,但是却是当前和vue配合做项目开发的一个比较好的ui框架,其包含了布局…...

计算机启动
按下主机上的 power 键后,第一个运行的软件是 BIOS,BIOS 全称叫 Base Input & Output System,即基本输入输出系统。 (8086的1MB内存) 地址 0~0x9FFFF 处是 DRAM,顶部的 0xF0000~0xFFFFF&am…...

Unity学习笔记--EventSystem事件系统在使用上需要注意的地方(很基础,但是很多人会忘记!!!)
目录 前言代码Unity 场景配置运行报错分析解决办法拓展(预告) 前言 之前有写过一篇关于事件系统实现以及使用的文章 Unity学习笔记–C#事件系统的实现与应用 最近在使用的时候遇到了一些问题,所以在此记录下,也为看到这篇文章的人…...

高手必备:JVM调优的常用命令和参数一网打尽!
大家好,我是小米!在今天的技术分享中,我将和大家一起探讨JVM调优中的常用命令和参数。作为一名热爱技术的小伙伴,希望通过本篇文章的分享,能够帮助大家更好地理解和掌握JVM调优的方法和技巧。 JVM的结构 首先&#x…...

Uniapp 开发 ①(快速上手)
作者 : SYFStrive 博客首页 : HomePage 📜: UNIAPP开发 📌:个人社区(欢迎大佬们加入) 👉:社区链接🔗 📌:觉得文章不错可以点点关注 Ǵ…...

【数据库原理与实践】知识点归纳(下)
第6章 规范化理论 一、关系模式设计中存在的问题 关系、关系模式、关系数据库、关系数据库的模式 关系模式看作三元组:R < U,F >,当且仅当U上的一个关系r满足F时,r称为关系模式R < U,F >的一个关系 第一范式(1NF&…...
代码随想录day34
1005.K次取反后最大化的数组和 本题主要是想到排序的时候要按绝对值大小排序。 class Solution { static bool cmp(int a,int b){return abs(a)>abs(b); } public:int largestSumAfterKNegations(vector<int>& nums, int k) {sort(nums.begin(),nums.end(),cmp);…...

CSS知识点汇总(八)--Flexbox
1. flexbox(弹性盒布局模型)是什么,适用什么场景? 1. flexbox(弹性盒布局模型)是什么 Flexible Box 简称 flex,意为”弹性布局”,可以简便、完整、响应式地实现各种页面布局。采用…...

ASCII、Unicode、UTF-8、GBK
入门小菜鸟,希望像做笔记记录自己学的东西,也希望能帮助到同样入门的人,更希望大佬们帮忙纠错啦~侵权立删。 目录 一、定义 1、ASCII 2、Unicode 3、UTF-8 4、GB2312 5、GBK 6、\u和\x 二、相互转化 1、str 与 ASCII 2、str与utf-…...

【安全】使用docker安装Nessus
目录 一、准备docker环境服务器(略) 二、安装 2.1 搜索镜像 2.2 拉取镜像 2.3 启动镜像 三、离线更新插件 3.1 获取challenge 3.2 官方注册获取激活码 3.3 使用challenge码和激活码获取插件下载地址 3.4 下载的插件以及许可协议复制到容器内 四…...

[ICLR 2022]How Much Can CLIP Benefit Vision-and-Language Tasks?
论文网址:pdf 英文是纯手打的!论文原文的summarizing and paraphrasing。可能会出现难以避免的拼写错误和语法错误,若有发现欢迎评论指正!文章偏向于笔记,谨慎食用 目录 1. 心得 2. 论文逐段精读 2.1. Abstract 2…...

使用 SymPy 进行向量和矩阵的高级操作
在科学计算和工程领域,向量和矩阵操作是解决问题的核心技能之一。Python 的 SymPy 库提供了强大的符号计算功能,能够高效地处理向量和矩阵的各种操作。本文将深入探讨如何使用 SymPy 进行向量和矩阵的创建、合并以及维度拓展等操作,并通过具体…...
Pydantic + Function Calling的结合
1、Pydantic Pydantic 是一个 Python 库,用于数据验证和设置管理,通过 Python 类型注解强制执行数据类型。它广泛用于 API 开发(如 FastAPI)、配置管理和数据解析,核心功能包括: 数据验证:通过…...
LangChain 中的文档加载器(Loader)与文本切分器(Splitter)详解《二》
🧠 LangChain 中 TextSplitter 的使用详解:从基础到进阶(附代码) 一、前言 在处理大规模文本数据时,特别是在构建知识库或进行大模型训练与推理时,文本切分(Text Splitting) 是一个…...

[拓扑优化] 1.概述
常见的拓扑优化方法有:均匀化法、变密度法、渐进结构优化法、水平集法、移动可变形组件法等。 常见的数值计算方法有:有限元法、有限差分法、边界元法、离散元法、无网格法、扩展有限元法、等几何分析等。 将上述数值计算方法与拓扑优化方法结合&#…...

基于stm32F10x 系列微控制器的智能电子琴(附完整项目源码、详细接线及讲解视频)
注:文章末尾网盘链接中自取成品使用演示视频、项目源码、项目文档 所用硬件:STM32F103C8T6、无源蜂鸣器、44矩阵键盘、flash存储模块、OLED显示屏、RGB三色灯、面包板、杜邦线、usb转ttl串口 stm32f103c8t6 面包板 …...

【Qt】控件 QWidget
控件 QWidget 一. 控件概述二. QWidget 的核心属性可用状态:enabled几何:geometrywindows frame 窗口框架的影响 窗口标题:windowTitle窗口图标:windowIconqrc 机制 窗口不透明度:windowOpacity光标:cursor…...

今日行情明日机会——20250609
上证指数放量上涨,接近3400点,个股涨多跌少。 深证放量上涨,但有个小上影线,相对上证走势更弱。 2025年6月9日涨停股主要行业方向分析(基于最新图片数据) 1. 医药(11家涨停) 代表标…...
Android多媒体——音/视频数据播放(十八)
在媒体数据完成解码并准备好之后,播放流程便进入了最终的呈现阶段。为了确保音视频内容能够顺利输出,系统需要首先对相应的播放设备进行初始化。只有在设备初始化成功后,才能真正开始音视频的同步渲染与播放。这一过程不仅影响播放的启动速度,也直接关系到播放的稳定性和用…...
TI德州仪器TPS3103K33DBVR低功耗电压监控器IC电源管理芯片详细解析
1. 基本介绍 TPS3103K33DBVR 是 德州仪器(Texas Instruments, TI) 推出的一款 低功耗电压监控器(Supervisor IC),属于 电源管理芯片(PMIC) 类别,主要用于 系统复位和电压监测。 2. …...