欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

Java final关键字在JMM中的含义

程序员文章站 2022-03-10 10:45:37
参考:[1] JSR 133 (Java Memory Model) FAQ[2] The JSR-133 Cookbook for Compiler Writers一、properly constructed / this对象逸出在开始讲之前final之前,先了解一个概念,叫做 “properly constructed”。其含义是:在构造器创建对象的过程中,正在被创建的对象的引用没有发生 “逸出(escape)” 。public Test {private final int...

参考:

一、properly constructed / this对象逸出

在开始讲之前final之前,先了解一个概念,叫做 “properly constructed”。其含义是:在构造器创建对象的过程中,正在被创建的对象的引用没有发生 “逸出(escape)” 。

public Test {
	private final int x;
	private int y;
	private static Test instance;
	
	public Test(int x, int y) {
		this.x = x;
		this.y = y;
		instance = this;
	}
}

在上面的例子中,在构造器中把正在创建的对象赋值给了一个静态变量instance,这种行为就叫“逸出”。

二、对象的安全发布

所谓对象的安全发布,意思就是构建一个被完整初始化的对象,防止一个未完全初始化的对象被访问。

看下面的例子:

public class Test {
	static MyObj obj;
	
	static class MyObj {
		int a, b, c, d;
		MyObj() {
			a = 1;
			b = 1;
			c = 1;
			d = 1;
		}
	}
	
	// Thread 1
	public static void init() {
		obj = new MyObj();
	}
	
	// Thread 2
	public static void f() {
		if (obj == null) return;
		if (obj != null && (obj.a + obj.b + obj.c + obj.d) != 4) 
			throw new RuntimeException();
	}

}

现在问你一个问题,对于上面的代码,一个线程执行init(),一个线程执行f(),最终有可能抛出异常吗?

答案是肯定的,有可能抛出异常,这就意味着线程2读到了未完全初始化的MyObj对象!

为什么会出现这种情况呢?是因为 obj = new MyObj() 这个操作底层实际上分为好几个步骤:

  1. 在堆上给MyObj对象分配空间,对象的字段置为默认值
  2. 执行构造器中的初始化语句进行初始化
  3. 将堆上的MyObj对象的引用赋值给obj

其中,步骤2和3是可以发生重排序的 (StoreStore重排序),因此就可能出现 obj != null,但是a,b,c,d还没全部赋值完成的情况,产生了未完全初始化的对象。

扩展一下,在实现单例模式时,会什么要double checked且加上volatile,正是这个原因,防止不安全的发布。

笔者在 x86 平台的 HotSpot 虚拟机中使用 jcstress 工具对不安全发布现象进行了测试,发现类似于上述的代码在 x86 平台的 HotSpot 虚拟机中并不会出现为完全初始化的情况,出现这种情况就说明,x86 平台的 HotSpot 虚拟机实现中,编译器没有进行StoreStore重排序,而x86的内存模型又保证了x86不会发生StoreStore重排序,因此上述步骤2,3不会发生重排序,进而始终可以读到完全初始化过的对象。但是,在其他硬件平台或其他JVM中就未必如此了。 我们的Java代码不应该基于特定硬件平台或虚拟机实现之上,而是应该遵循JLS和JMM的规范!

三、JMM中 final 关键字的语义

The values for an object’s final fields are set in its constructor. Assuming the object is constructed “correctly”, once an object is constructed, the values assigned to the final fields in the constructor will be visible to all other threads without synchronization. In addition, the visible values for any other object or array referenced by those final fields will be at least as up-to-date as the final fields.

在JMM中规定,如果我们在构造器中对final字段进行初始化,并且构造器中没有发生this对象的逸出,那么无需任何同步措施,即可确保其他线程可以看到构造器中初始化给final字段的值。

此外,如果这个final字段是一个引用类型,那么可以确保该引用类型对象里面的字段都被完全的赋值。

额外问一个问题,调用构造器的线程自己可以看到赋给final字段的值吗? 当然可以。无论是编译器还是硬件层面的重排序都遵循as-if-serial语义,对于单个线程,看起来就像是从前往后顺序执行的。

四、final 语义的实现参考

注意:重排序是针对单个线程(单个CPU)而言的。

以下摘自文档 [2]

1.A store of a final field (inside a constructor) and, if the field is a reference, any store that this final can reference, cannot be reordered with a subsequent store (outside that constructor) of the reference to the object holding that field into a variable accessible to other threads. For example, you cannot reorder
x.finalField = v; ... ; sharedRef = x;

其实这里的x最好理解成分配在堆上的对象(回顾前面new一个对象的3个步骤)。

看个例子就明白了:

public class Test {
	static Test instance;
	final int a;
	
	public Test() { a = 10; }
	
	public static void init() {
		instance = new Test();
	}
}

这个规则也就是说,给堆上Test对象的a进行初始化这个步骤将对上对象的引用赋值给 instance这个步骤,是不可以重排序的。实现这一点显然可以借助StoreStore屏障

This comes into play for example when inlining constructors, where “...” spans the logical end of the constructor. You cannot move stores of finals within constructors down below a store outside of the constructor that might make the object visible to other threads. (As seen below, this may also require issuing a barrier). Similarly, you cannot reorder either of the first two with the third assignment in:
v.afield = 1; x.finalField = v; ... ; sharedRef = x;

如果final字段是一个引用变量,则也可以确保对这个引用变量的字段的赋值也不会和sharedRef = x进行重排序。

2.The initial load (i.e., the very first encounter by a thread) of a final field cannot be reordered with the initial load of the reference to the object containing the final field. This comes into play in:
x = sharedRef; ... ; i = x.finalField;
A compiler would never reorder these since they are dependent, but there can be consequences of this rule on some processors.

再看个例子:

public class Test {
	static Test instance;
	final int a;
	
	public Test() { a = 10; }
	
	public static void init() {
		instance = new Test();
	}
	
	public static void read() {
		if (obj != null) {
			System.out.println(obj.a);
		}
	}
}

这条规则的意思是,通过对象访问其final字段(obj.a)这一操作该操作之前访问该对象的操作 (if (obj != null)) 是不能重排序的,这点可以通过加入LoadLoad屏障来实现。

本文地址:https://blog.csdn.net/qq_29328443/article/details/107672783

相关标签: Java并发