并发基础之Java内存模型、指令重排与数据正确性

在不改变程序执行结果的前提下,尽可能提高并行度。

Java 内存模型

JMM 就是 Java 内存模型,由于 Java 默认支持多线程操作,每个线程中都有自己的缓存,即下图中的本地内存(本地内存并不真实存在)。

JMM 通过控制主内存与每个线程的本地内存之间的交互,实现内存可见性的保证。

JMM

指令重排

重排序分为三种类型,分别为编译器优化的重排序、指令级并行的重排序和内存系统的重排序。只有第一种属于编译器重排序,剩下的都为处理器重排序。对于编译器,JMM 的编译器重排序规则会禁止特定类型的编译器重排序。对于处理器排序,JMM 的处理器重排序规则会要求 Java 编译器在生成指令时,插入特定类型内存屏障指令来禁止特定类型的处理器重排序。

数据正确性

并发编程最重要的核心就是数据正确,JMM 一定程度上保证了数据可见性,内存屏障又保证了指令执行顺序的正确性,这一切都是为了最后的数据正确。

数据依赖性

如果两个操作访问同一个变量,且这两个操作中又有一个为写操作,此时这两个操作之间就存在数据依赖性。分为 写后读、写后写和读后写三种情况。

如果对这三种情况进行重排序,执行结果就会出现错误。编译器和处理器在重排序时会遵守数据依赖性。但不同处理器和不同线程之间的数据依赖性不被保护。

as-if-serial 语义:不管如何重排序,(单线程)程序的执行结果不能被改变。编译器、处理器都遵守 as-if-serial 语义。

重排序对多线程的影响

当编译器和处理器对代码进行重排序时,可能会导致执行结果错误。

	class ReorderExample {
	    int a = 0;
	    boolean flag = false;
	    
		public void writer() {
			a = 1;       // 1
			flag = true; // 2
		}
	
		public void reader() {
		    if(flag) {         // 3
		        int i = a * a; // 4
		    }
		}
	}

在多线程场景下,进行重排序后,线程 A 先执行操作 2,线程 B 正常执行的情况下,线程 B 中的 i 的结果就为 0,多线程程序的语义被重排序破坏了。

操作依赖性

上面代码中,操作 3 和操作 4 存在控制依赖关系。当代码存在控制依赖关系时,会影响执行序列执行的并行度。编译器和处理器会采取猜测(Speculation)执行来克服控制相关性对并行度影响。以处理器的猜测执行为例,执行线程 B 的处理器可以提前读取并计算 a*a,然后把计算结果临时保存到名为重排序缓冲(Reorder Buffer,ROB)的硬件缓存中。当判断条件为真时,再把计算记过写入变量 i 中。

顺序一致性

顺序一致性模型是理想化的参考模型,有以下两点特性:

  • 一个线程中的所有操作必须按照程序的顺序来执行。
  • 无论程序是否同步,所有线程都只能看到一个单一的操作执行顺序(即所有线程所看到的操作执行顺序一致)。在顺序一致性内存模型中,每个操作必须原子执行且立刻对所有线程可见。

顺序一致性模型一种执行效果
但 JMM 中无法保证。未同步程序在 JMM 中不但整体的执行顺序是无序的,而且所有线程看到的操作执行顺序也可能不一致。

正确的多线程同步程序的执行将具有顺序一致性(程序的执行结果与该程序在顺序一致性内存模型中的执行结果相同)。

JMM 对数据安全性的保证

JMM 中在获取锁和释放锁的临界区内,可以进行重排序,但锁之间无法重排序。

对于未同步或未正确同步的多线程程序,JMM 只提供最小安全性:线程执行时读取到的值,要么是之前某个线程写入的值,要么是默认值。JMM 保证线程读操作读取到的值不会无中生有。

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值