【企业级理解】高效并发之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观察到,影响包括修改了内存中共享变量的值、发送了消息调用了方法等。

创作不易,点个赞吧~👍

最后的最后送大家一句话

白驹过隙,沧海桑田

与君共勉


相关文章
|
16小时前
|
缓存 监控 Java
Java一分钟之-Apache Geode:分布式内存数据平台
【5月更文挑战第21天】Apache Geode是低延迟的分布式内存数据平台,用于构建实时应用,提供缓存、数据库和消息传递功能。本文聚焦于Geode的常见问题,如数据一致性(数据同步延迟和分区冲突)和性能瓶颈(网络延迟和资源管理不当),并提出解决方案。确保数据一致性可通过选择合适的数据策略和利用`InterestPolicy`、`CacheListener`;提升性能则需优化网络和合理配置资源。通过示例代码展示了如何创建和操作Geode的Region。正确配置和调优Geode对于实现高可用、高性能应用至关重要。
15 1
|
1天前
|
监控 NoSQL Java
java云MES 系统源码Java+ springboot+ mysql 一款基于云计算技术的企业级生产管理系统
MES系统是生产企业对制造执行系统实施的重点在智能制造执行管理领域,而MES系统特点中的可伸缩、信息精确、开放、承接、安全等也传递出:MES在此管理领域中无可替代的“王者之尊”。MES制造执行系统特点集可伸缩性、精确性、开放性、承接性、经济性与安全性于一体,帮助企业解决生产中遇到的实际问题,降低运营成本,快速适应企业不断的制造执行管理需求,使得企业已有基础设施与一切可用资源实现高度集成,提升企业投资的有效性。
28 5
|
3天前
|
算法 Java 程序员
Java中的线程同步与并发控制
【5月更文挑战第18天】随着计算机技术的不断发展,多核处理器的普及使得多线程编程成为提高程序性能的关键。在Java中,线程是实现并发的一种重要手段。然而,线程的并发执行可能导致数据不一致、死锁等问题。本文将深入探讨Java中线程同步的方法和技巧,以及如何避免常见的并发问题,从而提高程序的性能和稳定性。
|
3天前
|
安全 Java 容器
Java一分钟之-并发编程:并发容器(ConcurrentHashMap, CopyOnWriteArrayList)
【5月更文挑战第18天】本文探讨了Java并发编程中的`ConcurrentHashMap`和`CopyOnWriteArrayList`,两者为多线程数据共享提供高效、线程安全的解决方案。`ConcurrentHashMap`采用分段锁策略,而`CopyOnWriteArrayList`适合读多写少的场景。注意,`ConcurrentHashMap`的`forEach`需避免手动同步,且并发修改时可能导致`ConcurrentModificationException`。`CopyOnWriteArrayList`在写操作时会复制数组。理解和正确使用这些特性是优化并发性能的关键。
10 1
|
3天前
|
安全 Java 容器
Java一分钟之-高级集合框架:并发集合(Collections.synchronizedXXX)
【5月更文挑战第18天】Java集合框架的`Collections.synchronizedXXX`方法可将普通集合转为线程安全,但使用时需注意常见问题和易错点。错误的同步范围(仅同步单个操作而非迭代)可能导致并发修改异常;错误地同步整个集合类可能引起死锁;并发遍历和修改集合需使用`Iterator`避免`ConcurrentModificationException`。示例代码展示了正确使用同步集合的方法。在复杂并发场景下,推荐使用`java.util.concurrent`包中的并发集合以提高性能。
17 3
|
4天前
|
移动开发 前端开发 JavaScript
Java和web前端,IT新人该如何选择?,2024年最新Web前端内存优化面试
Java和web前端,IT新人该如何选择?,2024年最新Web前端内存优化面试
|
5天前
|
存储 算法 Java
Java一分钟之-Java内存模型与垃圾回收机制概览
【5月更文挑战第16天】本文简述Java内存模型(JMM)和垃圾回收(GC)机制。JMM包括栈、堆、方法区、程序计数器和本地方法栈。GC负责回收不再使用的对象内存,常用算法有新生代、老年代和全堆GC。文章讨论了内存溢出、死锁和GC性能等问题,提出了解决方案,如调整JVM参数和优化GC策略。此外,还强调了避免内存泄漏、大对象管理及正确释放资源的重要性。理解这些概念有助于提升Java应用的性能和稳定性。
14 1
|
6天前
|
存储 安全 算法
掌握Java并发编程:Lock、Condition与并发集合
掌握Java并发编程:Lock、Condition与并发集合
14 0
|
6天前
|
存储 算法 Java
了解Java内存管理与垃圾回收机制
了解Java内存管理与垃圾回收机制
8 0
|
6天前
|
Java
Java并发Futures和Callables类
Java程序`TestThread`演示了如何在多线程环境中使用`Futures`和`Callables`。它创建了一个单线程`ExecutorService`,然后提交两个`FactorialService`任务,分别计算10和20的阶乘。每个任务返回一个`Future`对象,通过`get`方法获取结果,该方法会阻塞直到计算完成。计算过程中模拟延迟以展示异步执行。最终,打印出10!和20!的结果。
24 10