文档章节

Java的内存模型

o
 osc_1ee7cxmx
发布于 2018/08/07 07:52
字数 4198
阅读 14
收藏 0

钉钉、微博极速扩容黑科技,点击观看阿里云弹性计算年度发布会!>>>

Java的内存模型

概念

Java内存模型的主要目标是定义程序中各个变量的访问规则,即在虚拟机中将变量存储到内存中取出变量(这的变量包括实例字段。静态字段和构成数组对象的元素)这样的底层细节。

为了获得较好的执行效能。Java内存模型没有限制执行引擎使用处理器的特定寄存器或缓存来和主内存进行交互,也没有限制即时编译器进行调整代码执行顺序这类优化措施。

Java内存模型规定了所有的变量(前面的变量)都存储在主内存(Main Memory)中,每条线程还有自己的工作内存(Working Memory),线程的工作内存中保存了被该线程使用到的变量的主内存副本拷贝,线程对变量的所有操作(读取,赋值等等)都必须在工作内存中进行,而不能直接读写主内存中的变量,不同的线程之间也无法直接访问对方工作内存中的变量,线程间变量的传递都依靠内存完成。线程、主内存、工作内存的关系如图: 主内存和工作内存之间的交互协议(变量拷贝到工作内存,再从工作内存同步回主内存),Java内存模型定义了8种操作来完成,虚拟机实现必须保证每一个操作都是原子性、不可再分的(double和long例外)。

* lock(锁定):作用于主内存的变量,它把一个变量标识为一条线程独占的状态。
* unlock(解锁):作用于主内存的变量,它把一个处于锁定状态的变量释放出来,释放后的变量才可以被其他线程锁定。
* read(读取):作用于主内存的变量,它把一个变量的值从主内存传输到线程的工作内存中,以便随后的load动作使用。
* load(载入):作用于工作内存的变量,它把read操作从主内存中得到的变量值放入工作内存的变量副本中。
* use(使用):作用于工作内存的变量,它把工作内存中一个变量的值传递给执行引擎,每当虚拟机遇到一个需要使用到变量的值的字节码指令时将会执行这个操作。
* assign(赋值):作用于工作内存的变量,它把一个从执行引擎接收到的值赋给工作内存的变量,每当虚拟机遇到一个给变量赋值的字节码指令时执行这个操作。
* store(存储):作用于工作内存的变量,它把工作内存中一个变量的值传送到主内存中,以便随后的 write操作使用。
* wirte(写入):作用于主内存的变量,他把store操作从工作内存中得到的变量的值放入主内存的变量中。

变量从主内存到工作内存,必须经过read和load,从 工作内存同步回主内存,需要经过store和write操作,这两个操作必须按顺序执行,但不一定是连续执行。也就是在两个操作之间可以有其他的操作。 在使用上面的8中操作的时候需要满足的规则 :

* 不允许read和load、 store和 write操作之一单独出现,即不允许一个变量从主内存读取了但工作内存不接受,或者从工作存发起回写了但主内存不接受的情况出现。
* 不允许一个线程丢弃它的最近的 assign操作,即变量在工作内存中改变了之后必须把该变化同步回主内存。
* 不允许一个线程无原因地(没有发生过任何 assign操作)把数据从线程的工作内存同步回主内存中。
* 一个新的变量只能在主内存中“诞生”,不允许在工作内存中直接使用一个未被初始化(load或 assign)的变量,换句话说,就是对一个变量实施use、 store操作之前必须先执行过了 assign和load操作。
* 一个变量在同一个时刻只允许一条线程其进行1ock操作,但lock操作可以被同一条线程重复执行多次,多次执行lock后,只有执行相同次数的 unlock操作,变量被解锁。
* 如果对一个变量执行lock操作,那将会清空工作内存中此变量的值,在执行引擎使用这个变量前,需要重新执行load或 assign操作初始化变量的值。
* 如果一个变量事先没有被lock操作锁定,那就不允许对它执行unlock操作,也不允许去 unlock一个被其他线程锁定住的变量。
* 对一个变量执行 unlock操作之前,必须先把此变量同步回主内存中(执行stre、wite操作)。

Java内存模型要求的8个操作都具有原子性,但是对于64位的数据类型(long和double),定义了宽松的规定:允许虚拟机将没有被volitale修饰的64位数据的读写操作划分为两次32位的操作来进行,也就是虚拟机实现选择可以不保证64位数据类型的load、store、read和write这四个操作的原子性,这个就是所谓的long和double的非原子性协定。

内存模型的介绍

重排序

如果两个操作访问同一个变量,且这两个操作中有一个为写操作,此时这两个操作之间就存在数据依赖性。编译器和处理器可能会对操作做重排序,但是不会改变存在数据依赖关系的两个操作的执行顺序。遵循as-if-serial语义:即不管程序怎么重排序(编译器和处理器为了提高并行度),(单线程)程序的执行结果不能被改变,但在多线程中,对存在控制依赖的操作重排序,可能会改变程序的执行结果。

volatile型变量的规则

volatile是Java虚拟机提供的最轻量级的同步机制。当一个变量被定义为volatile类型后具备两种特性 :第一是保证此变量对所有线程的可见性,这里的“可见性”是指当一条线程改了这个变量的值,新值对于其他线程来说是可以立即得知的。而普通变量不能做到这一点,普通变量的值在线程间传递均需要通过主内存来完成,例如,线程A修改一个普通变量的值,然后向主内存进行回写。 第二是一条线程B在线程A回写完成了之后再从主内存进行读取操作,新变量值才会对线程B可见。

使用volatile变量的第一个语义是只保证可见性,有些场景下不适用,这个时候仍然需要通过加锁来保证原子性。不符合的运算场景:一是运算结果并不依赖变量的当前值,或者能够确保只有单一的线程修改变量的值。二是变量不需要与其他的状态变量共同参与不变约束。

第二个语义是禁止指令重排序优化,前面的重排序优化说过,编译器和处理器为了提高并行度,会对程序进行重排序,不能保证变量赋值的操作顺序与程序代码中的执行顺序一致。在使用volatile之后,在进行对volatile修饰的变量进行赋值后,会多执行一个lock操作,这个操作相当于一个内存屏障(Memory Barrier或Memory Fence,指重排序时不能把后面的指令重排序到内存屏障之前的位置),只有一个CPU访问是不需要这个内存屏障的,但是多个CPU 访问同一块内存,且其中一个在观测另一个,就需要内存屏障来保证一致性了。

Java内存模型中对 volatile变量定义的特殊规则。假定T表示一个线程,V和W分别表示两个 volatile型变量,那么在进行read、load、use、 assign store和 write操作时需要满足如下规则:

* 只有当线程T对变量V执行的前一个作是load的时候,线程T才能对变量V执行use动作;并且,只有当线程T对变量V执行的后一个动作是use的时候,线程T才能对变量V执行load动作。线程T对变量V的use动作可以认为是和线程T对变量V的load、read动作相关联,必须连续一起出现(这条规则要求在工作内存中,每次使用v前都必须先从主内存刷新最新的值,用于保证能看见其他线程对变量V所做的修改后的值)。
* 只有当线程T对变量V执行的前一个动作是assign的时候,线程T才能对变量V执行store动作;并且,只有当线程T对变量V执行的后一个动作是store的时候,线程T才能对变量V执行assign动作。线程T对变量V的assign动作可以认为是和线程T对变量V的store、 write动作相关联,必须连续一起出现(这条规则要求在工作内存中每次修改V后都必须立刻同步回主内存中,用于保证其他线程可以看到自己对变量v所做的修改)。
* 假定动作A是线程T对变量V实施的use或assign动作,假定动作F是和动作A相关联的load或 store动作,假定动作P是和动作F相应的对变量V的read或 wrte动作;类似的,假定动作B是线程T对变量W实施的use或 assign动作,假定动作是和动作B相关联的load或 store动作,假定动作Q是和动作G相应的对变量WErad或 write a动作。如果A先于B,那么P先于Q(这条规则要求 volatile 修饰的变量不会被指令重排序优化,保证代码的执行顺序与程序的顺序相同)。

原子性

Java内存模型来直接保证的原子性变量操作包括read、load、assign, use、 store和 wrte,我们大致可以认为基本数据类型的访问读写是具备原子性的(例外就是long和 double的非原子性协定),如果应用场景需要一个更大范围的原子性保证,Java内存模型还提供lock和 unlock操作来满足这种需求,尽管虚拟机未把lock和 unlock操作直接开放给用户使用,但是却提供了更高层次的字节码指令 monitorenter和 monitorexit来隐式地使用这两个操作,这两个字节码指令反映到Java代码中就是同步块— synchronized关键字,因此在synchronized块之间的操作也具备原子性。

可见性

可见性是指当一个线程修改了共享变量的值,其他线程能够立即得知这个修改。前面说 volatile变量的时候说过这一点。Java内存模型是通过在变量修改后将新值同步回主内存,在变量读取前从主内存刷新变量值。这种依赖主内存作为传递媒介的方式来实现可见性,无论是普通变量还是 volatile变量都是如此,普通变量与volatile变量的区别是, volatile的特殊规则保证了新值能立即同步到主内存,以及每次使用前立即从主内存刷新。因此,可以说 volatile保证了多线程操作时变量的可见性,而普通变量则不能保证这一点。

除了 volatile之外,Java还有两个关键字能实现可见性,即 synchronized和 final。同步块的可见性是由“对一个变量执行 unlock作之前,必须先把此变量同步回主内存中(执行store、 write操作)”这条规则获得的,而final关键字的可见性是指:被 final修饰的字段在构造器中一旦初始化完成,并且构造器没有把“this”的引用传递出去(this引用逃逸是一件很危险的事情,其他线程有可能通过这个引用访问到“初始化了一半”的对象),那在其他线程中就能看见 final字段的值。

有序性

Java内存模型的有序性在前面说volatile时也说过了,Java程序中天然的有序性可以总结为一句话:如果在本线程内观察,所有的操作都是有序的;如果在一个线程中观察另一个线程,所有的操作都是无序的。前半句是指“线程内表现为串行的语义”( Within- Thread As-If- Serial Semantics),后半句是指“指令重排序”现象和“工作内存与主内存同步延迟”现象。

Java语言提供了 volatile和 synchronized 两个关键字来保证线程之间操作的有序性 ,volatile关键字本身就包含了禁止指令重排序的语义,而 synchronized则是由“一个变量在同个时刻只允许一条线程对其进行lock操作”这条规则获得的,这条规则决定了持有同一个锁的两个同步块只能串行地进入。

先行发生原则

先行发生是Java内存模型中定义的两项操作之间的偏序关系,如果说操作A先行发生于操作B,其实就是说在发生操作B之前,操作A产生的影响能被操作B观察到,“影响”包括修改了内存中共享变量的值、发送了消息、调用了方法等。

下面是Java内存模型下一些“天然的”先行发生关系,这些先行发生关系无须任何同步协助就已经存在,可以在编码中直接使用。如果两个操作之间的关系不在此列,并且无法下列规则推导出来的话,它们就没有顺序性保障,虚拟机可以对它们随意地进行重排序 :

  • 程序次序规则( Program Order Rule):在一个线程内,按照程序代码顺序,书写在前面的操作先行发生于书写在后面的操作。准确地说,应该是控制流顺序而不是程序代码顺序,因为要考虑分支、循环等结构。
  • 管程锁定规则( Monitor Lock Rule):一个 unlock操作先行发生于后面对同一个锁的lock操作。这里必须强调的是同一个锁,而“后面”是指时间上的先后顺序。
  • volatile变量规则( Volatile Variable Rul):对一个 volatile变量的写操作先行发生于后面对这个变量的读操作,这里的“后面”同样是指时间上的先后顺序。
  • 线程启动规则( Thread Start Rule): Thread对象的 start()方法先行发生于此线程的每一个动作。
  • 线程终止规则( Thread Termination Rul):线程中的所有操作都先行发生于对此线程的终止检测,我们可以通过 Thread.join()方法结束、 Thread.isAlive()的返回值等手段检测到线程已经终止执行。
  • 线程中断规则( Thread Interruption Rul):对线程 interrupt()方法的调用先行发生于被中断线程的代码检测到中断事件的发生,可以通过 Thread.interrupted()方法检测到是否有中断发生。
  • 对象终结规则( Finalizer Rule):一个对象的初始化完成(构造函数执行结束)先行发生于它的 finalize()方法的开始。
  • 传递性(Transitivity):如果操作A先行发生于操作B,操作B先行发生于操作C,那就可以得出操作A先行发生于操作C的结论。

在Java程序中,时间先后顺序与先行发生原则之间基本没有太大的关系,所以衡量并发安全问题的时候不要受到时间顺序的干扰,一切必须以先行发生原则为准。

o
粉丝 0
博文 500
码字总数 0
作品 0
私信 提问
加载中
请先登录后再评论。

暂无文章

java架构师成长路线-高并发网络编程的分类

鲁班学院java架构师成长路线 随着互联网时代的到来,高并发网络编程这一新鲜名词早已跃然于纸上,为了满足大众眼光的需求,我为大家找了些关于高并发网络编程方面的资料,本文便来介绍高并发...

osc_o494ayqf
16分钟前
0
0
python dict乱码如何解决

定义字典并直接输出,结果输出结果中文是乱码展示 d={'name':'lily','age':18,'sex':'女','no':1121}print d 输出结果: {'age': 18, 'no': 1121, 'name': 'lily', 'sex': '\xe5\xa5\xb3'}...

osc_9mjo6c4e
17分钟前
7
0
硬肝50天,18w字的实战编程资料《重学Java设计模式》终于 出炉了

沉淀、分享、成长,让自己和他人都能有所收获! 一、前言 作者从5月20日那天投身实战型设计模式打磨,通过模拟互联网业务开发实际需求作为学习场景,讲解设计模式。 全书共计22个真实业务场景...

osc_zls6dx9i
19分钟前
20
0
怎么才能让Spring AOP有最大的作用--乐字节java

Spring AOP 日志处理带来的问题 我们有一个Pay(接口) 然后两个实现类DollarPay和RmbPay,都需要重写pay()方法, 这时我们需要对pay方法进行性能监控,日志的添加等等怎么做? 最容易想到的方法...

osc_sb30h1xb
20分钟前
10
0
Python 实现将numpy中的nan和inf,nan替换成对应的均值

nan:not a number inf:infinity;正无穷 numpy中的nan和inf都是float类型 t!=t 返回bool类型的数组(矩阵) np.count_nonzero() 返回的是数组中的非0元素个数;true的个数。 np.isnan() 返回b...

osc_sfl7wfr9
22分钟前
9
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部