JUC并发编程——JAVA内存模型

简介: JUC并发编程——JAVA内存模型

正文


一、CPU缓存结构


       由于CPU的运算速度比主存(物理内存)的存取速度快很多,为了提高处理速度,现代CPU不直接和内存进行通信,而是在CPU和主存之间设计了高速缓存(Cache),越靠近CPU层的高速缓存速度越快,容量越小。如下图


222.png


每一级高速缓存中所存储的数据都是下一级高速缓存中的一部分,L1最靠近CPU所以读取速度最快。L1和L2高速缓存只能被一个单独的CPU内核使用,L3高速缓存可以被同一个CPU芯片上的所有CPU内核共享,而主内存可以由系统中所有的CPU共享。CPU读取数据时,首先从L1层高速缓存中读取数据,如果没有读取到,再到L2,L3高速缓存中读取数据,如果都没有读取到数据,就会去主存中读取数据。


二、并发编程的三大问题


原子性


       原子性就是指不可中断的一个或者一些列操作,不能被线程调度机制打断的操作。如下面代码


public class Test01 {
    private int count=0;
    public void increase(){
        System.out.println(count++);
    }
}


将该java文件通过一下指令编译成class文件。javac -encoding UTF-8 Test01.java。通过javap反编译。


333.png


可知++操作并不是原子性的,而是进行了取值,运算,赋值,返回四个操作,在多线程并发的情况下,会发生原子性的问题,所以不是线程安全的。


可见性


一个线程对共享变量的修改,另一个线程能够立即可见,那么这个变量具有内存可见性。


JAVA内存模型


Java内存模型只是一种规范,抽象的概念,不是具体存在的。注意和JVM内存结构不同。


java内存模型规定


1、所有的变量存储在主内存中


2、每一个线程都有自己的工作内存,且对变量的操作都是在自己的工作内存中进行的。


3、不同的线程之间无法直接访问彼此工作内存中的变量,要想访问只能通过主存来传递(线程通信)


222.png


在java中,所有的局部变量,方法定义的参数都不会在线程之间共享,所以也就没有可见性的问题。而定义在堆中的共享变量,类,数组元素等都是线程共享的,存在可见性的问题。解决可见性问题可以使用volatile关键字解决。


有序性


       有序性是指程序执行的顺序按照代码的先后顺序执行。但是在执行程序时为了提高性能,编译器和处理器常常会对指令做重排序,它不保证程序各个语句的执行顺序严格按照代码顺序执行,但是它会保证程序最终的结果和顺序执行结果一致。但是这只是针对单线程运行,如果在多线程条件下,重排序会出现内存可见性问题,导致线程不安全。


重排序分三种类型


1、编译器优化的重排序。编译器在不改变单线程程序语义的前提下,可以重新安排语句的执行顺序。


2、指令级并行的重排序。现代处理器采用了指令级并行技术(Instruction-Level Parallelism, ILP)来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。


3、内存系统的重排序。由于处理器使用缓存和读 / 写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。


JAVA中synchronized、volatile可以解决重排序。


三、JMM内存模型


       JMM规定将所有的变量(不包括局部变量)都存放在公共内存中,当线程使用变量时会把主存的变量复制到自己的工作空间(私有内存),线程对变量的读写操作,都是在自己的工作空间内完成的。操作完成之后,再把自己私有内存的变量刷新到主内存中。但是如果两个或者多个线程同时操作同一个变量就会发生可见性问题。


       JMM定义了一套自己的主存与工作内存之间的交互协议,即变量如何从主存到工作内存,又如何从工作内存写入到主内存。该协议有8种操作,并且要求JVM具体实现必须保证其中每一种操作都是原子性的。


222.png


这八种操作必须满足以下规则


不允许read和load、store和Write操作单独出现。以上两个操作必须按照顺序执行,但没有保证必须连续执行,也就是在read/load、store/write之间可以插入其他指令。

不允许一个线程丢弃它最近的assign操作,也就是线程使用assign赋值之后,必须Write到主存中。

不允许一个线程无原因(没有assign操作)把数据从线程的工作内存同步到主存中。

一个新的变量只能从主内存中诞生,不允许在工作内存中直接使用一个未被初始化(load或assign)的变量,也就是说一个变量必须经过load和assign之后行use和store。

一个变量在同一时刻只允许一个线程给它执行lock操作。但lock操作可以被同一个线程重复执行多次,多次执行lock后,只有执行相同次数的unlock操作,才会释放对象。

如果对一个变量执行lock,将会清空工作内存中此变量的值,执行引擎使用这个变量前,需要重新load或者assign操作初始化变量的值。

如果一个变量事先没有被lock锁定,就不允许执行unlock,也不允许unlock被其他线程锁定的变量。

对一个变量执行unlock操作之前,必须把此变量同步回主存(执行 store和Write)


四、JMM如何解决有序性问题


       JMM提供了自己的内存屏障指令,要求JVM编译器实现这些指令,禁止特定类型的编译器和CPU重排序(不是所有的编译器重排序都要禁用)。


       JMM内存屏障主要有Load和Store两类


Load Barrier(读屏障):在读指令前插入读屏障,可以让高速缓存中的数据失效,重新从主存中加载数据。


Store Barrier(写屏障):在写指令之后插入写屏障,能让写入缓存的最新数据写回主存。


但是在实际使用时,会对Load Barrier和Store Barrier 组合使用


LoadLoad(LL)屏障:在执行预加载的指令序列中,通常需要显示的声明LoadLoad屏障,因为这些Load指令可能会依赖其他CPU执行的Load指令结果。


StoreStore(SS)屏障:通常情况下CPU不能保证从高速缓存向主存按顺序刷新数据,那么就需要SS,屏障。例如 store1;storestore;store2; 在Store2以及后续的写入操作之前,可以及时看到Store1对变量的操作。


LoadStore(LS)屏障:该屏障用于在数据写入操作执行前确保完成数据读取。


StoreLoad(SL)屏障:该屏障用于数据在读取执行操作之前,确保完成数据的写入。是开销最大,兼具其他三种屏障效果,在现代的多核CPU大多支持该屏障。


参考:


《JAVA高并发核心编程(卷2):多线程、锁、JMM、JUC、高并发设计》-尼恩编著

Java 并发 - 理论基础 | Java 全栈知识体系

相关文章
|
21天前
|
存储 Java 编译器
Java内存模型(JMM)深度解析####
本文深入探讨了Java内存模型(JMM)的工作原理,旨在帮助开发者理解多线程环境下并发编程的挑战与解决方案。通过剖析JVM如何管理线程间的数据可见性、原子性和有序性问题,本文将揭示synchronized关键字背后的机制,并介绍volatile关键字和final关键字在保证变量同步与不可变性方面的作用。同时,文章还将讨论现代Java并发工具类如java.util.concurrent包中的核心组件,以及它们如何简化高效并发程序的设计。无论你是初学者还是有经验的开发者,本文都将为你提供宝贵的见解,助你在Java并发编程领域更进一步。 ####
|
2天前
|
Java
java内存区域
1)栈内存:保存所有的对象名称 2)堆内存:保存每个对象的具体属性 3)全局数据区:保存static类型的属性 4)全局代码区:保存所有的方法定义
8 1
|
16天前
|
缓存 算法 Java
本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制
在现代软件开发中,性能优化至关重要。本文聚焦于Java内存管理与调优,介绍Java内存模型、内存泄漏检测与预防、高效字符串拼接、数据结构优化及垃圾回收机制。通过调整垃圾回收器参数、优化堆大小与布局、使用对象池和缓存技术,开发者可显著提升应用性能和稳定性。
36 6
|
20天前
|
存储 缓存 安全
Java内存模型(JMM):深入理解并发编程的基石####
【10月更文挑战第29天】 本文作为一篇技术性文章,旨在深入探讨Java内存模型(JMM)的核心概念、工作原理及其在并发编程中的应用。我们将从JMM的基本定义出发,逐步剖析其如何通过happens-before原则、volatile关键字、synchronized关键字等机制,解决多线程环境下的数据可见性、原子性和有序性问题。不同于常规摘要的简述方式,本摘要将直接概述文章的核心内容,为读者提供一个清晰的学习路径。 ####
35 2
|
21天前
|
存储 安全 Java
什么是 Java 的内存模型?
Java内存模型(Java Memory Model, JMM)是Java虚拟机(JVM)规范的一部分,它定义了一套规则,用于指导Java程序中变量的访问和内存交互方式。
49 1
|
3月前
|
存储 编译器 C语言
【C语言篇】数据在内存中的存储(超详细)
浮点数就采⽤下⾯的规则表⽰,即指数E的真实值加上127(或1023),再将有效数字M去掉整数部分的1。
385 0
|
29天前
|
存储 C语言
数据在内存中的存储方式
本文介绍了计算机中整数和浮点数的存储方式,包括整数的原码、反码、补码,以及浮点数的IEEE754标准存储格式。同时,探讨了大小端字节序的概念及其判断方法,通过实例代码展示了这些概念的实际应用。
60 1
|
1月前
|
存储
共用体在内存中如何存储数据
共用体(Union)在内存中为所有成员分配同一段内存空间,大小等于最大成员所需的空间。这意味着所有成员共享同一块内存,但同一时间只能存储其中一个成员的数据,无法同时保存多个成员的值。
|
1月前
|
存储 弹性计算 算法
前端大模型应用笔记(四):如何在资源受限例如1核和1G内存的端侧或ECS上运行一个合适的向量存储库及如何优化
本文探讨了在资源受限的嵌入式设备(如1核处理器和1GB内存)上实现高效向量存储和检索的方法,旨在支持端侧大模型应用。文章分析了Annoy、HNSWLib、NMSLib、FLANN、VP-Trees和Lshbox等向量存储库的特点与适用场景,推荐Annoy作为多数情况下的首选方案,并提出了数据预处理、索引优化、查询优化等策略以提升性能。通过这些方法,即使在资源受限的环境中也能实现高效的向量检索。
|
1月前
|
存储 编译器
数据在内存中的存储
数据在内存中的存储
42 4
下一篇
无影云桌面