【企业级理解】高效并发之Java内存模型

简介: 【企业级理解】高效并发之Java内存模型

在讲解java内存模型之前我们有必要先了解一下物理计算机中的并发问题

一、硬件的效率与一致性

我们知道计算机的处理器肯定要与内存进行交互,如读取运算数据,存储运算结果等,这个IO操作是很难消除的(无法仅靠寄存器来完成所有运算任务)。由于计算机的存储设备与处理器的运算速度有着几个数量级的差距,所以现代计算机都不得不加入一层或多层读写速度尽可能接近处理器运算速度的高速缓存(Cache)来作为内存与处理器之前的缓存:将运算要使用的数据复制到缓存中,让运算能快速进行,当运算结束后再从缓存同步回内存之中,这样处理器就无须等待缓慢的内存读写了

由于引进了高速缓存,因此引入了一个新问题:缓存一致性问题,在多路处理器系统中,每个处理器都有自己的高速缓存,而他们又同时共享同一主内存,这种系统称为共享内存多核系统。当多个处理器的运算任务都涉及同一块主内存区域时,将可能导致各自的缓存数据不一致;那我们如何解决呢:需要各个处理器访问缓存时都遵循一些协议,在读写时需要根据协议来操作,这类协议有MSI、MESI、MOSI、Synapse、Firefly及Dragon Protocol等。

二、Java内存模型

《Java虚拟机规范》中曾试图定义一种“java内存模型”来屏蔽各种硬件和操作系统的内存访问差异,以实现让java程序在各种平台下都能达到一致的内存访问效果。

2.1 主内存与工作内存

java内存模型的主要目的是定义程序中的各种变量的访问规则,即关注在虚拟机中把变量存储到内存和从内存中取出变量值这样的底层细节。此处的变量与java编程中的变量还是有点区别的,它包括了实例字段、静态变量、静态字段和构成数组对象的原素,但是不包括局部变量与方法参数,因为后者是线程私有的,不会被共享,也就不会存在竞争问题。

主内存:java内存模型规定了所有的变量都存储在主内存中

工作内存:每条线程都有自己的工作内存,线程的工作内存中被该线程使用的变量的主内存副本,线程对变量的所有操作(读取赋值等)都必须在工作内存中进行,而不能直接读写主内存中的数据。不通线程之间也无法直接访问对方工作内存中的变量,线程间变量值的传递均需要通过主内存来完成。

2.2 内存间的交互操作

关于主内存和工作内存之间具体的交互协议,即一个变量如何从主内存拷贝到工作内存,如何从工作内存同步回主内存这一类的实现细节,java内存模型定义了一下8种操作来完成,具体如下:

  • lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态
  • unlock(解锁):作用于主内存的变量,他把一个锁定的变量释放,释放的变量才能被其他线程锁定
  • read(读取):作用于主内存的变量,它把一个变量值从主内存传输到线程的工作内存中,以便随后的load动作使用
  • load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中
  • use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用变量的值的字节码指令时将会执行这个操作
  • assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收的值赋值给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时将会执行这个操作
  • store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的write操作使用
  • write(写入):作用于主内存的变量,他把store操作从工作内存中得到的变量的值放入主内存的变量中

如果要把一个变量从主内存拷贝到工作内存,那就要按顺序执行read和load操作,如果要把变量从工作内存同步回主内存,就要按顺序执行store和write操作。

2.3 volatile型变量的特殊规则

关键字volatile是java虚拟机提供的最轻量级的同步机制,但它在代码中使用的并不多,接下来我们就来了解一下

当一个变量被修饰为volatile后,它具有两项特征:第一项是保证此变量对所有线程的可见性这里的可见性是指当一条线程修改了这个变量的值,新值对于其他线程来说是可以立即得知的。而普通变量并不能做到这一点,普通变量的值在线程间传递均需要通过主内存来完成。比如,线程A修改了一个普通变量的值,然后向主内存进行回写,另外一条线程B在线程A写完了之后再对主内存进行读取,新变量的值才会对线程B可见。

volatile的一致性问题:从物理存储的角度看,各个线程的工作内存中volatile变量也可以存在不一致的情况,但由于每次使用前都要先刷新,执行引擎看不到不一致的情况,因此可以认为不存在一致性的问题。但是java中的运算操作符并非原子操作,这导致volatile变量的运算在并发下一样是不安全的。

这里简单举例下并发下使用volatile的线程不安全行为:

通过volatile修饰一个静态变量i,对于一个非原子操作,比如 i++ ,反编译这行代码会得到只有一行代码的increase()方法在Class文件中是由四条字节码指令构成的,当getstatic指令把 i 的值取到操作栈顶时,volatile关键字保证了 i 的值在此时是正确的,但是在执行iconst_1,iadd这些指令的时候,其他线程可能已经把i的值改变了,而操作栈顶的值就变成了过期的数据。

那么什么时候适合用volatile关键字呢?

  • 运算结果并不依赖变量的当前值,或者能够确保只有单一的线程来修改变量的值
  • 变量不需要与其他的状态变量共同参与不变约束

使用volatile变量的第二个语义是禁止指令重排优化,他的大概实现为:在volatile修饰的变量,修饰完之后相当于给变量增加了一个内存屏障,指令重排时不能把后面的指令重排序到内存屏障之前的位置。

关于指令重排的解释从硬件架构上讲,指令重排时指处理器采用了允许将多条指令不按程序规定的顺序分开发送给各个相应的电路单元进行处理。

其他并发情况,我们仍然需要通过加锁来保证原子性

volatile的性能

volatile变量读操作的性能消耗与普通变量几乎没有多大区别,但是写操作可能会慢上一些,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行(指令重排)

2.4 原子性、可见性与有序性

原子性

由java内存模型来直接保证原子性变量操作包括:read、load、assign、use、store和write这六个,我们大致可以认为基本类型的访问和读写都具备原子性。如果应用场景需要一个更大的范围来保证原子性,java内存模型还提供了lock和unlock操作来满足这种需求,虚拟机提供了lock和unlock的字节码指令monitorenter和monitorexit来隐示的使用这两个操作,这两个字节码反应到java代码中就是同步代码块–synchroized关键字,因此在synchroized代码块中也具备原子性。

可见性

可见性是指当一个线程修改了共享变量的值时,其他线程能够立即得知这个修改。

java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值这种依赖主内存作为传递媒介的方式来实现可见性的,无论是普通变量和volatile都是。

普通变量和volatile变量的区别是:volatile的特殊规则保证了新值能立即同步回主内存,以及每次使用前立即从主内存刷新,因此volatile保证了多线程操作时变量的可见性

有序性

java语言提供了volatile和synchronized两个关键字来保证线程之间操作的有序性,volatile关键字本身就包含了禁止指令重排的语义,而synchronized则是由一个变量在同一时刻只允许一条线程对其进行lock操作这条规则获得的,这个规则决定了持有一个锁的两个同步块只能串行进入。

2.4 先行发生原则

java语言中有一个先行发生的原则,这个原则非常重要,它是判断数据是否存在竞争,线程是否安全的非常有用的手段。

什么是先行发生原则?

先行发生原则是java内存模型中定义的两项操作之间的偏序关系,比如说操作A先行发生于操作B,其实就是说在发生操作B之前,操作A产生的影响被B观察到,影响包括修改了内存中共享变量的值、发送了消息调用了方法等。

创作不易,点个赞吧~👍

最后的最后送大家一句话

白驹过隙,沧海桑田

与君共勉


相关文章
|
11天前
|
缓存 easyexcel Java
Java EasyExcel 导出报内存溢出如何解决
大家好,我是V哥。使用EasyExcel进行大数据量导出时容易导致内存溢出,特别是在导出百万级别的数据时。以下是V哥整理的解决该问题的一些常见方法,包括分批写入、设置合适的JVM内存、减少数据对象的复杂性、关闭自动列宽设置、使用Stream导出以及选择合适的数据导出工具。此外,还介绍了使用Apache POI的SXSSFWorkbook实现百万级别数据量的导出案例,帮助大家更好地应对大数据导出的挑战。欢迎一起讨论!
103 1
|
3天前
|
Java 数据库连接 数据库
如何构建高效稳定的Java数据库连接池,涵盖连接池配置、并发控制和异常处理等方面
本文介绍了如何构建高效稳定的Java数据库连接池,涵盖连接池配置、并发控制和异常处理等方面。通过合理配置初始连接数、最大连接数和空闲连接超时时间,确保系统性能和稳定性。文章还探讨了同步阻塞、异步回调和信号量等并发控制策略,并提供了异常处理的最佳实践。最后,给出了一个简单的连接池示例代码,并推荐使用成熟的连接池框架(如HikariCP、C3P0)以简化开发。
13 2
|
6天前
|
存储 运维 Java
💻Java零基础:深入了解Java内存机制
【10月更文挑战第18天】本文收录于「滚雪球学Java」专栏,专业攻坚指数级提升,希望能够助你一臂之力,帮你早日登顶实现财富自由🚀;同时,欢迎大家关注&&收藏&&订阅!持续更新中,up!up!up!!
21 1
|
9天前
|
存储 算法 Java
Java虚拟机(JVM)的内存管理与性能优化
本文深入探讨了Java虚拟机(JVM)的内存管理机制,包括堆、栈、方法区等关键区域的功能与作用。通过分析垃圾回收算法和调优策略,旨在帮助开发者理解如何有效提升Java应用的性能。文章采用通俗易懂的语言,结合具体实例,使读者能够轻松掌握复杂的内存管理概念,并应用于实际开发中。
|
16天前
|
存储 监控 算法
Java中的内存管理与垃圾回收机制解析
本文深入探讨了Java编程语言中的内存管理方式,特别是垃圾回收机制。我们将了解Java的自动内存管理是如何工作的,它如何帮助开发者避免常见的内存泄漏问题。通过分析不同垃圾回收算法(如标记-清除、复制和标记-整理)以及JVM如何选择合适的垃圾回收策略,本文旨在帮助Java开发者更好地理解和优化应用程序的性能。
|
18天前
|
存储 Java
Java内存模型
【10月更文挑战第11天】Java 内存模型(JMM)是 Java 虚拟机规范中定义的多线程内存访问机制,解决内存可见性、原子性和有序性问题。它定义了主内存和工作内存的概念,以及可见性、原子性和有序性的规则,确保多线程环境下的数据一致性和操作正确性。使用 `synchronized` 和 `volatile` 等同步机制可有效避免数据竞争和不一致问题。
29 3
|
18天前
|
缓存 安全 Java
使用 Java 内存模型解决多线程中的数据竞争问题
【10月更文挑战第11天】在 Java 多线程编程中,数据竞争是一个常见问题。通过使用 `synchronized` 关键字、`volatile` 关键字、原子类、显式锁、避免共享可变数据、合理设计数据结构、遵循线程安全原则和使用线程池等方法,可以有效解决数据竞争问题,确保程序的正确性和稳定性。
31 2
|
20天前
|
Java
【编程进阶知识】揭秘Java多线程:并发与顺序编程的奥秘
本文介绍了Java多线程编程的基础,通过对比顺序执行和并发执行的方式,展示了如何使用`run`方法和`start`方法来控制线程的执行模式。文章通过具体示例详细解析了两者的异同及应用场景,帮助读者更好地理解和运用多线程技术。
24 1
|
22天前
|
Java 数据挖掘 数据库连接
Java使用直接内存的好处
综上所述,Java直接内存的使用为开发者提供了一种绕过JVM堆限制、直接高效操作内存资源的途径,特别适用于高吞吐量、低延迟和大规模数据处理的场景。虽然直接内存的使用需要更精细的管理以避免内存泄漏和过度消耗系统资源,但恰当的利用能够显著提升应用的性能表现,是现代高性能Java应用不可或缺的工具之一。
26 2
|
5天前
|
SQL 监控 数据可视化
完全开源!国内首个完全开源JAVA企业级低代码平台
JeeLowCode 是一款专为企业打造的 Java 企业级低代码开发平台,通过五大核心引擎(SQL、功能、模板、图表、切面)和四大服务体系(开发、设计、图表、模版),简化开发流程,降低技术门槛,提高研发效率。平台支持多端适配、国际化、事件绑定与动态交互等功能,广泛适用于 OA、ERP、IoT 等多种管理信息系统,帮助企业加速数字化转型。