Java内存模型-底层原理
JMM是什么
是一组规范,需要各个JVM的实现来遵守JMM规范,以便于开发者可以利用这些规范,更方便地开发多线程程序。
如果没有这样的一个JMM内存模型来规范,那么很可能经过了不同JVM的不同规则的重排序之后,导致不同的虚拟机上运行的结果不一样,那是很大的问题。
volatile、synchronized、lock的的原理都是JMM
如果没有JMM,那么就需要我们自己指定什么时候用内存栅栏等,那是相当麻烦的,幸好有了JMM,让我们只需要用同步工具和关键字就可以开发并发程序。
什么是重排序
在线程1内部的两行代码的实际执行顺序和代码在Java文件中的顺序不一致,代码指令并不是严格按照代码语句顺序执行的,它们的顺序被改变了,这就是重排序,这里被颠倒的是y=a和b=1这两行语句。
重排序的好处:提高处理速度
- 对比重排序前后的优化
重排序的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虚拟机中不会出现
原子操作 + 原子操作 !=原子操作
简单地把原子操作组合在一起,并不能保证整体依然具有原子性
可见性
为什么需要JMM
- C语言不存在内存模型的概念
- 依赖处理器,不同处理器结果不一样
- 无法保证并发安全
- 需要一个标注,让多线程运行的结果可预期
为什么会有可见性问题
CPU有多级缓存,导致读的数据过期
- 如果所有核心都只用一个缓存,那么也就不存在内存可见性问题了。
- 每个核心都会将自己需要的数据读到独占缓存中,数据修改后也是写入到缓存中,然后等待刷入到主存中。所以会导致有些核心读取的值是一个过期的值。
JMM的抽象:主内存和本地内存
Java作为高级语言,屏蔽了这些底层细节,用JMM定义了一套读写内存数据的规范,虽然我们不需要关心一级缓存和二级缓存的问题,但是,JMM抽象了主内存和本地内存的概念。
本地内存:这里说的本地内存并不是真的是一块给每个线程分配的内存,而是JMM一个抽象,是对于寄存器、一级缓存二级缓存等的抽象。
JMM有以下规定:
- 所有的变量都存储在主内存中,同时每个线程也有自己独立的工作内存,工作内存中的变量内容是主内存的拷贝。
- 线程不能直接读写主内存中的变量,而是只能操作自己工作内存中的变量,然后再同步到主内存中。
- 主内存是多个线程共享的,但线程间不共享工作内存,如果线程间需要通信,必须借助主内存中转来完成。
结论:所有的共享变量存在于主内存中,每个线程有自己的本地内存,而且线程读写共享数据也是通过本地内存交换的,所以才导致了可见性问题。
Happens-Before原则
什么是Happens-Before?
- happens-before规则是用来解决可见性问题的:在时间上,动作A发生在动作B之前,B保证能看见A,这就是happens-before。
- 两个操作可以用happens-before来确定它们的执行顺序:如果一个操作happens-before于另一个操作,那么我们说第一个操作对于第二个操作是可见的。
什么不是happens-before
两个线程没有互相配合的机制,所以代码X和Y的执行结果并不能保证总被对方看到的,这就不具备happens-before。
happens-before规则有哪些?
- 单线程规则
- 锁操作(synchronized和Lock)
- volatile变量
- 线程启动
- 线程join
- 传递性
- 中断:一个线程被其他线程interrupt是,那么检查中断(isInterrupted)或者抛出InterruptedException一定能看到。
- 构造方法
工具类的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小结
- volatile修饰符适用以下场景:某个属性被多个线程共享,其中有一个线程修改了此属性,其他线程可以立即得到修改后的值,比如boolean flag;或者作为触发器,实现轻量级同步。
- volatile属性的读写操作都是无锁的,它不能替代synchronized,因为它没有提供原子性和互斥性。因为无锁,不需要花费时间在获取锁和释放锁上,所以说它是低成本的。
- volatile只能作用于属性,我们用volatile修饰属性,这样compilers就不会对这个属性做指令重排序。
- volatile提供了可见性,任何一个线程对其的修改将立马对其他线程可见。volatile属性不会被线程缓存,始终从主存中读取。
- volatile提供了happen-before保证,对volatile变量V的写入happen-before所有其他线程后续对v的读操作
- volatile可以使得long和double的赋值是原子的,后面
常见面试问题
为什么需要单例?
- 节省内存和计算
- 保证结果正确
- 方便管理
单例模式适用场景?
- 无状态的工具类:比如日志工具类,不管是在哪里适用,我们需要的只是它帮我们记录日志信息,除此之外,并不需要在它的实例对象上存储任何状态,这时候我们就只需要一个实例对象即可。
- 全局信息类:比如我们在一个类上记录网站的访问次数,我们不希望有的访问被记录在对象A上,有的却记录在对象B上,这时候我们就让这个类成为单例。
单例模式8中写法?
- 饿汉式(静态常量)可用
- 饿汉式(静态代码块)可用
- 懒汉式(线程不安全)
- 懒汉式(线程安全,同步方法)[不推荐]
- 懒汉式(线程不安全,同步代码块)[不可用]
- 双重检查[推荐用]
优点:线程安全;延迟加载;效率高效
为什么要double-check
- 线程安全
- 单check行不行?会出现重复初始化
- 性能问题
为什么要用volatile
- 新建对象实际上有3个步骤
- 重排序会带来空指针问题
- 防止重排序
- 静态内部类[推荐用]
- 枚举[推荐用]
写法简单
线程安全有保障
避免反序列化破坏单例
讲一讲什么是Java内存模型?
1.起因 2.java内存模型,java内存结构,java
volatile和synchronized的异同?
什么是原子操作?java中有哪些原子操作?生成对象的过程是不是原子操作?
- 新建一个空的Person对象
- 把这个对象的地址指向p
- 执行Person的构造函数
什么是内存可见性?
64 位的double和long写入的时候是原子的吗?