Java内存模型JMM与顺序一致性模型关于的对比与内存可见性讨论

本文将主要围绕内存可见性展开讨论,并横向对比其与顺序一致性内存模型的异同,探究Java内存模型在可见性问题上是如何规范的。其中在Java内存模型中内存可见性部分,主要涉及指令重排序相关的可见性问题,数据缓存相关的可见性问题不重点介绍。

数据竞争

要想深入理解常被提及的内存可见性,就必须先了解可见性由何而来。

在多线程环境下,当程序未做合适的同步处理,就有可能出现数据竞争的情况,所谓数据竞争就是指并发条件下的状态属性不同步而引发的读写不一致问题,即假设现有两个线程要同时对一个变量进行操作,线程A进行写操作,线程B进行读操作,而系统没有对两线程做同步处理(排序),此处线程B所读到的变量就有可能有两种状态,且结果是不可预测的。

由于并发环境下数据竞争问题的存在,内存可见性问题应运而生,Java中有许多用于解决可见性问题的方法,比如:

  • 使用volatile关键字禁止部分特定类型的指令重排序以解决可见性问题
  • 使用synchronized关键字进行同步处理,保证对某变量同时刻只有一个线程能够操作已解决可见性问题
  • 使用final关键字确保变量的不变性以解决可见性问题

如何解决可见性问题并非文本讨论重点,一语带过,接下来关注顺序一致性模型和Java内存模型在此问题上是如何规范的。

顺序一致性模型

首先顺序一致性模型并不是某个具体投入使用的模型,顺序一致性模型是一个理想化的理论参考模型,用于在设计实际投入使用的模型时进行参照,是一个主要针对内存可见性规范的模型。

特征

可以说顺序一致性模型最大限度的保证的内存可见性,顺序一致性模型有两大特性:

  1. 一个线程中的所有操作必须按照程序的顺序来执行。
  2. 所有线程都只能看到一个单一的操作执行顺序。每个操作都必须原子执行且立刻对所有线程可见。

实现

为了保证上述的两大特定,在顺序一致性模型中划定一个唯一的全局内存,所有线程读写操作的目标都被保存在这个全局内存中,每一个线程都必须按照程序的顺序来执行读/写操作,且同时刻最多只有一个线程可以访问全局内存,示意图如下所示:
在这里插入图片描述

并发效果

在顺序一致性模型中,多线程并发执行时,顺序一致性模型会对这些并发线程做串行化处理,同时保证每个线程内部操作的原子性,并且保证最终执行顺序唯一(即所有线程中都只能看到唯一的执行顺序,与指令重排序相关)。

正确同步下的并发执行效果

在程序正确同步的情况下,假设同时刻线程A和线程B并发执行,线程A中有三个操作,按照顺序分别是A1→A2→A3,线程B同样有三个操作,按照顺序分别未B1→B2→B3,执行效果如下图所示(结果不唯一):
在这里插入图片描述

无同步下的并发执行效果

除无同步,其余条件与正确同步状态下的假设相同,执行效果如下图所示(结果不唯一):
在这里插入图片描述

如上述二图可见,顺序一致性模型保证了:①最终执行顺序唯一(所有线程都只能看到这个执行顺序)②线程A和线程B的内各自的操作按顺序执行。在无同步状态下,虽然在整体在上看线程A、B的操作执行顺序无序,但具体到单一线程上的操作实际上是有序的(因为是理想化模型,不存在指令重排)。同时在顺序一致性模型中每个操作必须立即对所有线程可见,所以保证了所有线程看到的最终执行顺序都是一致的。

Java内存模型

本文不会详细完备的介绍JMM(Java内存模型简写),只会摘选与顺序一致性模型对比相关的部分。

首先是JMM的结构,与顺序一致性模型不同,JMM更像是处理器结构,每个线程有独立的本地内存用于存放共享变量的副本,线程直接与本地内存交互,而本地内存经有JMM控制直接与主存交互,主存内存有若干共享变量,JMM示意图如下:

在这里插入图片描述

重排序问题

前文已多次提及顺序一致性模型是一个理想化的模型,而指令重排序就是被理想化剔除问题之一。接下来简要的叙述重排序。

重排序就是指实际应用中,编译器和处理器为了优化程序性能而对指令序列进行重新排序的手段。重排序分为以下三类:

  1. 编译器优化的重排序:在不改变单线程程序语义的前提下,可以重新安排语句执行顺序。
  2. 指令级并行的重排序:如果不存在数据依赖,处理器可以改变语句对应机器指令的执行顺序。
  3. 内存系统的重排序:处理器对缓存读/写缓冲区的重排序。

例如下面这段简单的代码,实际执行中就有可能被重排序:

int a = 1;//A操作
int b = 2;//B
int c = a + b;//C

首先三行代码间存在着数据依赖关系,而数据依赖关系会影响重排序的结果(或是否对某个操作进行重排序),显而易见C依赖于A、B,因此操作C不会被处理器重排序到操作A、B之前,而操作A、B间并无数据依赖关系,A、B有可能被重排序(因为①无数据依赖关系②不会影响代码执行结果),因而有两种执行顺序ABC和BAC。

以上简单的例子是建立在单线程的基础上,即使代码重排序存在也不需要进行额外的同步处理就可以保证执行结果的正确,但在多线程环境下则有可能引发错误。

JMM对于重排序的约束

JMM中对于重排序问题,有as-if-serial语义进行约束,语义即:**不管编译器和处理器怎么重排序,(在单线程中)程序执行的结果不能被改变。**上述的编译器、处理器等都必须遵守as-if-serial语义。

编译器和处理器保证不会对存在数据依赖关系的操作进行重排序,因为必定会改变执行结果。

JMM对于数据竞争的约束

对于数据竞争问题,所有读操作和写操作都遵顼Happens-Before规则

  • 程序顺序规则。如果程序中操作A在操作B之前,那么在线程中A操作将在B操作之前执行。
  • 监视器锁规则。在监视器锁上的解锁操作必须在同一个监视器锁上的加锁操作之前执行。
  • volatile变量规则。对volatile变量的写入操作必须在对该变量的读操作之前执行。
  • 线程启动规则。在线程上对Thread.Start的调用必须在该线程中执行的任何操作之前执行。
  • 线程结束规则。线程中的任何操作都必须在其他线程检测到该线程已经结束之前执行,或者从Thread.join中成功返回,或者在调用Thread.isAlive时返回false。
  • 中断规则。当一个线程在另一个线程上调用interrupt时,必须在被中断线程检测到interrupt调用之前执行(通过抛出InterruptedException,或者调用isInterrupted和interrupted).
  • 终结器规则。对象的构造函数必须在启动该对象的终结器之前执行完成。
  • 传递性。如果操作A在操作B之前执行,并且操作B在操作C之前执行,那么操作A必须在操作C之前执行。

并发效果

在简要了解了JMM对指令重排以及数据竞争的约束之后,可以继续向下分析JMM中的并发效果。

正确同步下的并发执行效果

现在假设线程A执行writer()方法后,线程B执行reader()方法。,示例代码如下:

class Example{
	int a = 0;
	boolean flag = false;
	
	public synchronized void writer(){	//获取锁
		a = 1;
		flag = true;
	}									//释放锁
									
	public synchronized void reader(){	//获取锁
		if(flag){
			int i = a;
			......
		}
	}									//释放锁
}

由前文可知,在顺序一致性模型中,所有操作将完全按照程序顺序串行执行,而JMM允许临界区内的代码进行重排序,顺序一致性模型中及JMM执行时序对比图如下:
在这里插入图片描述

无同步下的并发执行效果

此处仍然以顺序一致性模型中的并发效果例为例,执行效果可能如下(结果不唯一):
在这里插入图片描述
经上文内容,易总结出JMM在未同步的程序执行过程中与内存可见性相关的执行特性

  1. JMM不保证单线程的擦欧总会按程序顺序执行
  2. JMM不保证所有线程都能看到一致的执行顺序

因此可以得出结论,在实际并发环境开发过程中,应当注意到程序中操作的执行顺序并非总是按照惯性思维像顺序一致性模型那样在单线程层面上顺序执行,应对不同的业务逻辑采取合适的同步处理,比如文中提及的使用synchronized、volatile或final关键字,避免可见性引发的并发问题。

顺序一致性模型与Java内存模型的对比

首先要明确顺序一致性模型是一个理想化的理论参考模型,他忽略了实际生产环境中指令重排序的问题,又因为顺序一致性模型的结构,避免了像JMM中由于数据缓存引起的可见性问题。

最后是前文未着重提及的,顺序一致性模型保证所有的内存读/写操作都具有原子性,这点在并发环境下的问题体现在对64位long型或double型变量的写操作上,因为在一些32位处理器上64位写操作可以细分为高32位写操作和低32位写操作,这两个操作并非是一定相连的,即此二操作直接可能掺杂读操作,引发并发问题。

总结

  1. 顺序一致性模型保证单线程内操作都会按照程序顺序执行,而JMM不保证单线程操作按照程序顺序执行。这点差异是由指令重排序导致。
  2. 顺序一致性模型保证所有线程都能看到一致且唯一的操作执行顺序,而JMM不保证所有简称能看到一致的操作执行顺序。这点差异是由于二者结构上的异同,JMM中与线程不能与主内存直接交互,必须先经手本地内存(也叫工作内存)。
  3. 顺序一致性模型保证所有的内存读/写操作都具有原子性,JMM不保证对64位long型或double型变量的写操作具有原子性

注明:部分内容、代码与图注参考/摘自《Java并发编程的艺术》、《Java并发编程实战》

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

7rulyL1ar

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值