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写入的时候是原子的吗?

相关文章
|
16天前
|
存储 缓存 安全
Java内存模型深度解析:从理论到实践####
【10月更文挑战第21天】 本文深入探讨了Java内存模型(JMM)的核心概念与底层机制,通过剖析其设计原理、内存可见性问题及其解决方案,结合具体代码示例,帮助读者构建对JMM的全面理解。不同于传统的摘要概述,我们将直接以故事化手法引入,让读者在轻松的情境中领略JMM的精髓。 ####
28 6
|
7天前
|
安全 Java 程序员
深入理解Java内存模型与并发编程####
本文旨在探讨Java内存模型(JMM)的复杂性及其对并发编程的影响,不同于传统的摘要形式,本文将以一个实际案例为引子,逐步揭示JMM的核心概念,包括原子性、可见性、有序性,以及这些特性在多线程环境下的具体表现。通过对比分析不同并发工具类的应用,如synchronized、volatile关键字、Lock接口及其实现等,本文将展示如何在实践中有效利用JMM来设计高效且安全的并发程序。最后,还将简要介绍Java 8及更高版本中引入的新特性,如StampedLock,以及它们如何进一步优化多线程编程模型。 ####
14 0
|
1月前
|
算法 JavaScript 前端开发
新生代和老生代内存划分的原理是什么?
【10月更文挑战第29天】新生代和老生代内存划分是JavaScript引擎为了更高效地管理内存、提高垃圾回收效率而采用的一种重要策略,它充分考虑了不同类型对象的生命周期和内存使用特点,通过不同的垃圾回收算法和晋升机制,实现了对内存的有效管理和优化。
|
9天前
|
存储 监控 算法
Java内存管理深度剖析:从垃圾收集到内存泄漏的全面指南####
本文深入探讨了Java虚拟机(JVM)中的内存管理机制,特别是垃圾收集(GC)的工作原理及其调优策略。不同于传统的摘要概述,本文将通过实际案例分析,揭示内存泄漏的根源与预防措施,为开发者提供实战中的优化建议,旨在帮助读者构建高效、稳定的Java应用。 ####
22 8
|
28天前
|
存储 算法 Java
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
本文详解自旋锁的概念、优缺点、使用场景及Java实现。关注【mikechen的互联网架构】,10年+BAT架构经验倾囊相授。
大厂面试高频:什么是自旋锁?Java 实现自旋锁的原理?
|
29天前
|
Java
Java之CountDownLatch原理浅析
本文介绍了Java并发工具类`CountDownLatch`的使用方法、原理及其与`Thread.join()`的区别。`CountDownLatch`通过构造函数接收一个整数参数作为计数器,调用`countDown`方法减少计数,`await`方法会阻塞当前线程,直到计数为零。文章还详细解析了其内部机制,包括初始化、`countDown`和`await`方法的工作原理,并给出了一个游戏加载场景的示例代码。
Java之CountDownLatch原理浅析
|
1月前
|
Java 索引 容器
Java ArrayList扩容的原理
Java 的 `ArrayList` 是基于数组实现的动态集合。初始时,`ArrayList` 底层创建一个空数组 `elementData`,并设置 `size` 为 0。当首次添加元素时,会调用 `grow` 方法将数组扩容至默认容量 10。之后每次添加元素时,如果当前数组已满,则会再次调用 `grow` 方法进行扩容。扩容规则为:首次扩容至 10,后续扩容至原数组长度的 1.5 倍或根据实际需求扩容。例如,当需要一次性添加 100 个元素时,会直接扩容至 110 而不是 15。
Java ArrayList扩容的原理
|
9天前
|
安全 Java 程序员
Java内存模型的深入理解与实践
本文旨在深入探讨Java内存模型(JMM)的核心概念,包括原子性、可见性和有序性,并通过实例代码分析这些特性在实际编程中的应用。我们将从理论到实践,逐步揭示JMM在多线程编程中的重要性和复杂性,帮助读者构建更加健壮的并发程序。
|
15天前
|
Java
java内存区域
1)栈内存:保存所有的对象名称 2)堆内存:保存每个对象的具体属性 3)全局数据区:保存static类型的属性 4)全局代码区:保存所有的方法定义
20 1
|
20天前
|
Java
Java内存模型
JMM(Java内存模型 )屏蔽了各种硬件和操作系统的内存访问差异,实现让Java程序在各平台下都能达到一致的内存访问效果,它定义了JVM如何将程序中的变量在主存中读取 具体定义为:所有变量都存在主存中,主存是线程共享区域;每个线程都有自己独有的工作内存,线程想要操作变量必须从主从中copy变量到自己的工作区,每个线程的工作内存是相互隔离的 由于主存与工作内存之间有读写延迟,且读写不是原子性操作,所以会有线程安全问题