volatile 可见性、有序性 的实现原理及 final 语义

以前文章

synchronized 锁的实现原理

文章目录

JMM

JMM定义了共享内存中多线程程序读写操作的行为规范:在虚拟机中把共享变量存储到内存
以及从内存中取出共享变量的底层实现细节。通过这些规则来规范对内存的读写操作从而保证指令的正
确性,它解决了CPU多级缓存、处理器优化、指令重排序导致的内存访问问题,保证了并发场景下的可
见性。

volatile

如果一个字段被声明为 volatile ,java 线程内存模型确保所有线程看到这个变量的值都是一致的。可见性和有序性

被 volatile 修饰的变量进行写操作时会多出一个 Lock 前缀的指令,该指令在多核处理器下会引发两件事情

  1. 将当前处理器缓存行的数据写回到系统内存。
  2. 这个写回内存的操作会使其他 CPU 里缓存了该内存地址的数据无效。

出现的问题:

当多核处理器进行写操作时,线程会将系统内存的数据加载的内部缓存中。当修改了变量的数值之后会存储在当前处理器缓存行中,对其他CPU 不可知。此时会造成其他线程访问时读取的是旧的数据造成可见性的问题。

如果对声明了 volatile 的变量进行写操作,JVM就会向处理器发送一条 Lock 前缀指令,将这个变量所在缓存行的数据写回到系统内存。就算写回到内存中,如果其他处理器缓存的值还是旧值的话,再执行计算操作就会有问题。所以在多处理器下,为了保证各个处理器的缓存是一致的,就会实现缓存一致性协议(MESI)。

MESI 缓存一致性协议

当 volatile 修饰的变量

  • 只有一个CPU使用时,此时缓存行的状态是 E 独占状态
  • 存在多个CPU使用时,此时缓存行的状态是 S 共享状态
  • 存在多个CPU使用时且某个CPU的缓存行做了修改,此时缓存行的状态是M 修改状态,并更新其他CPU中引用了该内存地址的数据为 I 失效状态。

指令重排序

为了提高性能,编译器和处理器常常会对指令做重排序。

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

1 属于编译器重排序,2和3 属于处理器重排序

这些重排序会导致多线程程序出现可见性的问题。为了解决这个问题,JMM 的处理器重排序规则会要求java 编译器在生成指令序列时,插入特定类型的内存屏障指令,通过内存屏障来禁止处理器重排序。

内存屏障

LoadLoad Barriers

StoreStore Barriers

LoadStore Barriers

StoreLoad Barriers

在这里插入图片描述

final

编译器和处理器要遵守两个重排序规则

  1. 在构造函数内对一个final 域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
  2. 初次读一个包含final 域的对象的引用,与随后初次读这个final 域,这两个操作之间不能重排序。

写 final 域的重排序规则

  1. JMM 禁止编译器把 final 域的写重排序到构造函数之外
  2. 编译器会在 final 域写入之后,构造函数 return 之前,插入一个 StoreStore 屏障。这个屏障禁止处理器把final 域的写重排序到构造函数之外。

当 执行一个 obj = new Object (); 时包含了两个步骤

  1. 构造一个 Object 类型的对象
  2. 把这个对象的引用赋值给引用变量 obj

写 final 域的重排序规则确保:在对象引用为任意线程可见之前,对象的final 域已经被正确初始化,而普通域不保证。

读 final 域的重排序规则

在一个线程中,初次读对象引用(即读对象)与初次读该对象包含的 final 域(即 读对象的 final 域) ,JMM 禁止处理器重排序这两个操作(这个规则仅仅针对处理器)。编译器会在读 final 域操作的前面插入一个 LoadLoad 屏障

读 final 域的重排序规则确保:再读一个对象的 final 域之前,一定会先读包含这个 final 域的对象的引用。如果该引用不为 null ,那么引用对象的 final 域一定是初始化过的。

final 域为引用类型

在构造函数内对一个final 引用的对象的成员域的写入(对象的 final 域的成员赋值),与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量(对象的引入赋值给其他引用变量),这个操作之间不能重排序。

这个不太好理解,意思是如果对象 A 的 final 引用类型的域的成员在构造函数内存在写入,不能将对象 A 的值赋值给其他引入对象

public A(){ // arr[] 是 final 域
    arr[] = int[1]; // 1
    arr[0]=1;  // 2
}
public void test{
   	A a = new A(); 
	A b = a; // 3 在 arr[0] 未操作完成前,不能进行该操作。
    // 正确步骤是 1 - 2 - 3
}

final 语义在处理器中的实现

写 final 域的重排序规则会要求编译器在 final 域之后,构造函数 return 之前插入一个 StoreStore 屏障

读 final 域的重排序规则要求编译器在读 final 域的操作前面插入一个 LoadLoad 屏障

但在 X86 处理器中,读 / 写都不会插入任何内存屏障。因为 X86 处理器不会对存在间接依赖关系的操作做重排序。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值