Java内存模型-底层原理

简介: Java内存模型-底层原理

Java内存模型-底层原理

JMM是什么

是一组规范,需要各个JVM的实现来遵守JMM规范,以便于开发者可以利用这些规范,更方便地开发多线程程序。

如果没有这样的一个JMM内存模型来规范,那么很可能经过了不同JVM的不同规则的重排序之后,导致不同的虚拟机上运行的结果不一样,那是很大的问题。

volatile、synchronized、lock的的原理都是JMM

如果没有JMM,那么就需要我们自己指定什么时候用内存栅栏等,那是相当麻烦的,幸好有了JMM,让我们只需要用同步工具和关键字就可以开发并发程序。

什么是重排序

在线程1内部的两行代码的实际执行顺序和代码在Java文件中的顺序不一致,代码指令并不是严格按照代码语句顺序执行的,它们的顺序被改变了,这就是重排序,这里被颠倒的是y=a和b=1这两行语句。

重排序的好处:提高处理速度

  • 对比重排序前后的优化

image-20210125165851507

重排序的3种情况

  • 编译器优化:包括JVM、JIT编译器等
  • CPU指令重排:就算编译器不发生重排,CPU也可能对指令进行重排
  • 内存的“重排序”:线程A的修改线程B却看不到,引出可见性问题

什么是原子性

一系列操作,要么全部执行成功,要么全部不执行,不会出现执行一半的情况,是不可分割的。

Java中的原子操作有哪些?

  • 除long和double之外的基本类型的赋值操作
  • 所有引用reference的赋值操作,不管是32位的机器还是64位的机器
  • java.concurrent.Atomic.* 包中所有类的原子操作

long和double的原子性
问题描述:官方文档、对于64位的值写入,可以分为两个32位的操作进行写入、读取错误、使用volatile解决

结论:在32位上的JVM上,long和double的操作不是原子的,但是在64位的JVM上是原子的

实际开发中:商用Java虚拟机中不会出现

原子操作 + 原子操作 !=原子操作

简单地把原子操作组合在一起,并不能保证整体依然具有原子性

可见性

image-20210126171157721

为什么需要JMM

  1. C语言不存在内存模型的概念
  2. 依赖处理器,不同处理器结果不一样
  3. 无法保证并发安全
  4. 需要一个标注,让多线程运行的结果可预期

为什么会有可见性问题

CPU有多级缓存,导致读的数据过期

  • 如果所有核心都只用一个缓存,那么也就不存在内存可见性问题了。
  • 每个核心都会将自己需要的数据读到独占缓存中,数据修改后也是写入到缓存中,然后等待刷入到主存中。所以会导致有些核心读取的值是一个过期的值。

JMM的抽象:主内存和本地内存

Java作为高级语言,屏蔽了这些底层细节,用JMM定义了一套读写内存数据的规范,虽然我们不需要关心一级缓存和二级缓存的问题,但是,JMM抽象了主内存和本地内存的概念。

本地内存:这里说的本地内存并不是真的是一块给每个线程分配的内存,而是JMM一个抽象,是对于寄存器、一级缓存二级缓存等的抽象。
image-20210126201625387

JMM有以下规定:

  1. 所有的变量都存储在主内存中,同时每个线程也有自己独立的工作内存,工作内存中的变量内容是主内存的拷贝。
  2. 线程不能直接读写主内存中的变量,而是只能操作自己工作内存中的变量,然后再同步到主内存中。
  3. 主内存是多个线程共享的,但线程间不共享工作内存,如果线程间需要通信,必须借助主内存中转来完成。

结论:所有的共享变量存在于主内存中,每个线程有自己的本地内存,而且线程读写共享数据也是通过本地内存交换的,所以才导致了可见性问题。

Happens-Before原则

什么是Happens-Before?

  1. happens-before规则是用来解决可见性问题的:在时间上,动作A发生在动作B之前,B保证能看见A,这就是happens-before。
  2. 两个操作可以用happens-before来确定它们的执行顺序:如果一个操作happens-before于另一个操作,那么我们说第一个操作对于第二个操作是可见的。

什么不是happens-before

两个线程没有互相配合的机制,所以代码X和Y的执行结果并不能保证总被对方看到的,这就不具备happens-before。

happens-before规则有哪些?

  1. 单线程规则
  2. 锁操作(synchronized和Lock)
  3. volatile变量
  4. 线程启动
  5. 线程join
  6. 传递性
  7. 中断:一个线程被其他线程interrupt是,那么检查中断(isInterrupted)或者抛出InterruptedException一定能看到。
  8. 构造方法
  9. 工具类的happens-before原则

    • 线程安全的容器get一定能看到在此之前的put等存入动作
    • CountDownLath
    • Semaphore
    • Future
    • 线程池
    • CyclicBarrier

volatie关键字

volatile是什么?

voliatile是一种同步机制,比synchronized或者Lock相关类更轻量,因为使用volatile并不会发生上下文切换等开销很大的行为。但是开销小,相应的能力也小,虽然说volatile是用来同步的保证线程安全的,但是volatile做不到synchroized那样的原子保护,volatile仅在很有限的场景下才能发挥作用。

volatile的适合场合?

适用场景1:boolean flag ,如果一个共享变量自始至终只被各个线程赋值,而没有其他的操作,那么就可以用volatile来代替synchronized或者代替原子变量,因为赋值自身是原子性的,而volatile又保证了可见性,所以就足以保证线程安全。
适用场合2:作为刷新之前变量的触发器

volatile的作用:可见性、禁止重排序

可见性:读一个volatile变量之前,需要先使相应的本地缓存失效,这样就必须到主内存读取最新值,写一个volatile属性会立即刷入到主内存
禁止指令重排序优化:解决单例双重锁乱序问题

volatile和synchronized的关系?

volatile在这方面可以看做是轻量版的synchronized:如果一个共享变量自始至终只被各个线程赋值,而没有其他的操作,那么就可以用volatile来代替synchronized或者代替原子变量,因为赋值自身是有原子性的,而volatile又保证了可见性,所有就足以保证线程安全

volatile小结

  1. volatile修饰符适用以下场景:某个属性被多个线程共享,其中有一个线程修改了此属性,其他线程可以立即得到修改后的值,比如boolean flag;或者作为触发器,实现轻量级同步。
  2. volatile属性的读写操作都是无锁的,它不能替代synchronized,因为它没有提供原子性和互斥性。因为无锁,不需要花费时间在获取锁和释放锁上,所以说它是低成本的。
  3. volatile只能作用于属性,我们用volatile修饰属性,这样compilers就不会对这个属性做指令重排序。
  4. volatile提供了可见性,任何一个线程对其的修改将立马对其他线程可见。volatile属性不会被线程缓存,始终从主存中读取。
  5. volatile提供了happen-before保证,对volatile变量V的写入happen-before所有其他线程后续对v的读操作
  6. volatile可以使得long和double的赋值是原子的,后面

常见面试问题

为什么需要单例?

  1. 节省内存和计算
  2. 保证结果正确
  3. 方便管理

单例模式适用场景?

  1. 无状态的工具类:比如日志工具类,不管是在哪里适用,我们需要的只是它帮我们记录日志信息,除此之外,并不需要在它的实例对象上存储任何状态,这时候我们就只需要一个实例对象即可。
  2. 全局信息类:比如我们在一个类上记录网站的访问次数,我们不希望有的访问被记录在对象A上,有的却记录在对象B上,这时候我们就让这个类成为单例。

单例模式8中写法?

  1. 饿汉式(静态常量)可用
  2. 饿汉式(静态代码块)可用
  3. 懒汉式(线程不安全)
  4. 懒汉式(线程安全,同步方法)[不推荐]
  5. 懒汉式(线程不安全,同步代码块)[不可用]
  6. 双重检查[推荐用]

优点:线程安全;延迟加载;效率高效
为什么要double-check

  1. 线程安全
  2. 单check行不行?会出现重复初始化
  3. 性能问题

为什么要用volatile

  1. 新建对象实际上有3个步骤
  2. 重排序会带来空指针问题
  3. 防止重排序
  1. 静态内部类[推荐用]
  2. 枚举[推荐用]
写法简单
线程安全有保障
避免反序列化破坏单例

讲一讲什么是Java内存模型?

1.起因 2.java内存模型,java内存结构,java

volatile和synchronized的异同?

什么是原子操作?java中有哪些原子操作?生成对象的过程是不是原子操作?

  1. 新建一个空的Person对象
  2. 把这个对象的地址指向p
  3. 执行Person的构造函数

什么是内存可见性?

64 位的double和long写入的时候是原子的吗?

相关文章
|
5天前
|
算法 Java Go
Go vs Java:内存管理与垃圾回收机制对比
对比了Go和Java的内存管理与垃圾回收机制。Java依赖JVM自动管理内存,使用堆栈内存并采用多种垃圾回收算法,如标记-清除和分代收集。Go则提供更多的手动控制,内存分配与释放由分配器和垃圾回收器协同完成,使用三色标记算法并发回收。示例展示了Java中对象自动创建和销毁,而Go中开发者需注意内存泄漏。选择语言应根据项目需求和技术栈来决定。
|
2天前
|
缓存 算法 内存技术
深入理解操作系统内存管理:原理与实践
【4月更文挑战第30天】本文旨在深入探讨操作系统中的内存管理机制,包括物理内存的分配与回收、虚拟内存技术、分页系统以及内存优化策略。通过对内存管理概念的详细解读和实际案例分析,读者将获得对操作系统如何处理内存资源的全面认识,并了解如何在实践中应用这些知识以提高系统性能和稳定性。
|
2天前
|
存储 Java
深入理解Java虚拟机:JVM内存模型
【4月更文挑战第30天】本文将详细解析Java虚拟机(JVM)的内存模型,包括堆、栈、方法区等部分,并探讨它们在Java程序运行过程中的作用。通过对JVM内存模型的深入理解,可以帮助我们更好地编写高效的Java代码,避免内存溢出等问题。
|
2天前
|
存储 机器学习/深度学习 Java
【Java探索之旅】数组使用 初探JVM内存布局
【Java探索之旅】数组使用 初探JVM内存布局
11 0
|
3天前
|
存储 算法 安全
深入理解操作系统内存管理:原理与实践
【4月更文挑战第29天】 在现代计算机系统中,操作系统的内存管理是其核心功能之一。有效的内存管理不仅关乎系统性能,也直接影响到用户程序的稳定性和安全性。本文将详细探讨操作系统内存管理的基本原理、关键技术以及当前的挑战和创新方向。通过对页式管理、段式管理和段页式管理等技术的深入分析,我们旨在为读者提供一个清晰、系统的内存管理知识框架,并讨论虚拟内存技术如何帮助解决物理内存不足的问题。同时,考虑到安全性日益成为关注焦点,文中还将介绍内存保护机制和内存隔离技术。最后,结合最新的硬件发展趋势,如非易失性内存(NVM)的出现,本文也将对内存管理的未来发展方向进行展望。
|
3天前
|
存储 安全 Java
【Java EE】CAS原理和实现以及JUC中常见的类的使用
【Java EE】CAS原理和实现以及JUC中常见的类的使用
|
3天前
|
存储 缓存
深入理解操作系统:虚拟内存管理的原理与实践
【4月更文挑战第28天】 在现代计算机系统中,虚拟内存是操作系统提供的一项重要功能,它允许程序使用比物理内存更大的地址空间。本文将深入探讨虚拟内存的工作原理,包括分页、分段和请求分页等技术,以及它们在操作系统中的应用。
|
4天前
|
安全 Java 编译器
Java面向对象思想以及原理以及内存图解(下)
Java面向对象思想以及原理以及内存图解(下)
11 0
|
4天前
|
Java
Java面向对象思想以及原理以及内存图解(上)
Java面向对象思想以及原理以及内存图解
14 0
|
5天前
|
存储 算法 安全
深入理解操作系统内存管理:原理与实践
【4月更文挑战第27天】 在现代计算机系统中,操作系统的内存管理是其核心功能之一,它负责协调和分配系统内存资源,确保程序高效稳定地运行。本文将详细探讨操作系统内存管理的基本原理、关键技术以及面临的挑战。通过对内存分配策略、分页机制、虚拟内存技术等关键概念的剖析,揭示操作系统如何优化内存使用,支持多任务并发执行,并保证系统的稳定与安全。同时,文中还将对最新的内存管理技术和趋势进行展望,为读者提供一个全面、深入的理解框架。