JavaJava的内存模型,我才知道是这样!!!
Posted Rose J
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了JavaJava的内存模型,我才知道是这样!!!相关的知识,希望对你有一定的参考价值。
前言
1.并发编程需要注意的两个问题
在并发编程中我们需要注意两个关键的问题:
1.线程之间如何通信
2.线程之间如何同步
(这里的线程是指并发执行的活动实体)
通信是指线程之间以何种机制来交换信息,在命令式编程中,线程之间的通信机制有两种:共享内存和消息传递
共享内存:
在共享内存的并发模型里,线程之间共享程序的公共状态,通过写-读内存中的公共状态进行隐式通信
消息传递:
在消息传递的并发模型里,线程之间没有公共状态,线程之间必须通过发送消
息来显式进行通信。
同步是指程序中用于控制不同线程间操作发生相对顺序的机制。
在共享内存并发模型里,同步是显式进行的。程序员必须显式指定某个方法或某段代码需要在线程之间互斥执行。
在消息传递的并发模型里,由于消息的发送必须在消息的接收之前,因此同步是隐式进行的。
Java的并发采用的是共享内存模型,Java线程之间的通信总是隐式进行,整个通信过程对程序员完全透明。
如果编写多线程程序的Java程序员不理解隐式进行的线程之间通信的工作机制,很可能会遇到各种奇怪的内存可见性问题。
2.Java内存模型的抽象结构
在Java中,所有实例域,静态域和数组元素都存储在对堆内存,堆内存在线程之间共享
这里可以对比一下Java的内存结构知识
Java线程之间的通信由Java内存模型(本文简称为JMM)控制,JMM决定一个线程对共享变量的写入何时对另一个线程可见。
从抽象的角度来看,JMM定义了线程和主内存之间的抽象关系:线程之间的共享变量存储在主内存(Main Memory)中,每个线程都有一个私有的本地内存(Local Memory),本地内存中存储了该线程以读/写共享变量的副本。本地内存是JMM的
一个抽象概念,并不真实存在。它涵盖了缓存、写缓冲区、寄存器以及其他的硬件和编译器优化。
从图3-1来看,如果线程A与线程B之间要通信的话,必须要经历下面2个步骤。
1)线程A把本地内存A中更新过的共享变量刷新到主内存中去。
2)线程B到主内存中去读取线程A之前已更新过的共享变量。
下面通过示意图(见图3-2)来说明这两个步骤
如图3-2所示,本地内存A和本地内存B由主内存中共享变量x的副本。假设初始时,这3个内存中的x值都为0。线程A在执行时,把更新后的x值(假设值为1)临时存放在自己的本地内存A中。当线程A和线程B需要通信时,线程A首先会把自己本地内存中修改后的x值刷新到主内存中,此时主内存中的x值变为了1。随后,线程B到主内存中去读取线程A更新后的x值,此时线程B的本地内存的x值也变为了1。
从整体来看,这两个步骤实质上是线程A在向线程B发送消息,而且这个通信过程必须要经过主内存。JMM通过控制主内存与每个线程的本地内存之间的交互,来为Java程序员提供内存可见性保证
Java内存模型介绍
他是啥嘞?
通俗来说,JMM是一套多线程读写共享数据时,对数据的可见性,有序性和原子性的规则
换句话说,JMM是一种抽象的概念,并不真实存在。他描述的是一组规则或者说是规范,通过这组规范定义了程序中各个变量(包括实例字段,静态字段或者数组对象的元素)的访问方式
由于JVM运行程序的实体是线程,而每个线程创建时JVM都会为其创建一个工作内存(有些地方称为栈空间),用于存储线程私有的数据,而Java内存模型中规定所有变量都存储在主内存,主内存是共享内存区域,所有线程都可以访问,但线程对变量的操作(读取赋值等)必须在工作内存中进行,首先要将变量从主内存拷贝的自己的工作内存空间,然后对变量进行操作,操作完成后再将变量写回主内存,不能直接操作主内存中的变量,工作内存中存储着主内存中的变量副本拷贝,前面说过,工作内存是每个线程的私有数据区域,因此不同的线程间无法访问对方的工作内存,线程间的通信(传值)必须通过主内存来完成,其简要访问过程如下图
线程,主内存,工作内存
为啥要有?
JVM实现不同会造成“翻译”的效果不同,不同CPU平台的机器指令有千差万别,无法保证同一份代码并发下的效果一致。所以需要一套统一的规范来约束JVM的翻译过程,保证并发效果一致性
原子性
1.原子性是啥?
原子性就是指一系列的操作,要么全部执行成功,要么全部不执行,不会出现一半的情况,是不可分的奥~
2.原子性怎么实现呢?
1.使用synchronized和lock加锁实现,保证任一时刻只有一个线程访问该代码块
2.使用原子操作
3.java中原子操作有哪些?
1.除long和double之外的基本数据的赋值操作(64位数,当成了两次32位 的进行操作)
2.所有引用reference的赋值操作
3.java.concurrent.Atomic.*包中所有类的原子操作
-
创建对象的过程是否是原子操作(常应用于双重检查+volatile创建单例场景)
创建对象实际上有3个步骤,并不是原子性的
- 创建一个空对象
- 调用构造方法
- 创建好的实例赋值给引用
可见性
1.什么是可见性问题?
可见性是指的当一个线程修改了某个共享变量的值,其他线程是否能够马上得知这个修改的值
2.为什么会有可见性问题出现?
1.对于单线程程序来说,可见性是不存在的,因为我们在任何一个操作中修改了某个变量的值,后续的操作中都能读取这个变量值,并且是修改过的新值。
2.对于多线程程序而言。由于线程对共享变量的操作都是线程拷贝到各自的工作内存进行操作后才写回到主内存中的,这就可能存在一个线程A修改了共享变量x的值,还未写回主内存时,另外一个线程B又对主内存中同一个共享变量x进行操作,但此时A线程工作内存中共享变量x对线程B来说并不可见,这种工作内存与主内存同步延迟现象就造成了可见性问题
3.如何解决可见性问题
解决方法1:加volatile关键字保证可见性。当一个共享变量被volatile修饰时,它会保证修改的值立即被其他的线程看到,即修改的值立即更新到主存中,当其他线程需要读取时,它会去内存中读取新值
解决方法2:使用synchronized和Lock保证可见性。因为它们可以保证任一时刻只有一个线程能访问共享资源,并在其释放锁之前将修改的变量刷新到内存中
有序性
1.什么是重排序
在线程内部的两行代码的实际执行顺序和代码在Java文件中的逻辑顺序不一致,代码指令并不是严格按照代码语句顺序执行的,他们的顺序被改变了,这就是重排序。
2.重排序的意义
JVM能根据处理器特性(CPU多级缓存系统、多核处理器等)适当的对机器指令进行重排序,使机器指令能更符合CPU的执行特性,最大限度的发挥机器性能。
3.重排序的3种情况
1.编译器优化( JVM,JIT编辑器等): 编译器在不改变单线程程序语义放入前提下,可以重新安排语句的执行顺序
2.指令级并行的重排序:现代处理器采用了指令级并行技术来将多条指令重叠执行。如果不存在数据依赖性,处理器可以改变语句对应机器指令的执行顺序。
3.内存系统的重排序: 由于处理器使用缓存和读写缓冲区,这使得加载和存储操作看上去可能是在乱序执行。
volatile
1.什么是volatile?
- volatile是一种同步机制,比synchronized或者Lock相关类更轻量级,因为使用volacile并不会发生上下文切换等开销很大的行为
- volatile是无锁的,并且只能修饰单个属性
2.什么时候适合用volatile?
- 一个共享变量始终只被各个线程赋值,没有其他操作
- 作为刷新的触发器,引用刷新之后使修改内容对其他线程可见(如CopyOnRightArrayList底层动态数组通过volatile修饰,保证修改完成后通过引用变化触发volatile刷新,使其他线程可见)
-
volatile的作用
可见性保障:修改一个volatile修饰变量之后,会立即将修改同步到主内存,使用一个volatile修饰的变量之前,会立即从主内存中刷新数据。保证读取的数据都是最新的,之前的修改都是可见的。
有序性保障(禁止指令重排序优化):有volatile修饰的变量,赋值后多了一个“内存屏障“( 指令重排序时不能把后面的指令重排序到内存屏障之前的位置)
在单例模式中双重检测可以用到。 -
volatile的性能
volatile 的读性能消耗与普通变量几乎相同,但是写操作稍慢,因为它需要在本地代码中插入许多内存屏障指令来保证处理器不发生乱序执行。
happens-before规则
从JDK 5开始,Java使用新的JSR-133内存模型(除非特别说明,本文针对的都是JSR-133内存模型)。JSR-133使用happens-before的概念来阐述操作之间的内存可见性。在JMM中,如果一个操作执行的结果需要对另一个操作可见,那么这两个操作之间必须要存在happens-before关系
什么是happens-before规则:前一个操作的结果可以被后续的操作获取。
1.程序的顺序性规则:在一个线程内一段代码的执行结果是有序的。虽然还会指令重排,但是随便它怎么排,结果是按照我们代码的顺序生成的不会变!
2.volatile规则: 就是如果一个线程先去写一个volatile变量,然后一个线程去读这个变量,那么这个写操作的结果一定对读的这个线程可见。
3.传递性规则:happens-before原则具有传递性,即A happens-before B , B happens-before C,那么A happens-before C。
3.管程锁定规则:无论是在单线程环境还是多线程环境,对于同一个锁来说,一个线程对这个锁解锁之后,另一个线程获取了这个锁都能看到前一个线程的操作结果!(管程是一种通用的同步原语,synchronized就是管程的实现)
4.线程启动规则:在主线程A执行过程中,启动子线程B,那么线程A在启动子线程B之前对共享变量的修改结果对线程B可见。
5.线程终止规则: 在主线程A执行过程中,子线程B终止,那么线程B在终止之前对共享变量的修改结果在线程A中可见。
6.线程中断规则: 对线程interrupt()方法的调用先行发生于被中断线程代码检测到中断事件的发生,可以通过Thread.interrupted()检测到是否发生中断。
7.对象终结规则:一个对象的初始化的完成,也就是构造函数执行的结束一定 happens-before它的finalize()方法。
“别怕失败,大不了重头来过,你懂得”
以上是关于JavaJava的内存模型,我才知道是这样!!!的主要内容,如果未能解决你的问题,请参考以下文章