之前的文章介绍volatile和锁的内存语义,今天我们来介绍下final的内存语义。与前面的锁和volatile相比,对final域的读写更像是普通变量的访问。
1.在构造函数内对一个final域得写入,与随后把这个被构造对象得引用赋值给一个引用变量,这两个操作之间不能重排序。
2.初次读一个包含final域得对象得引用,与之后初次读这个final域,这两个操作之间不能重排序。
/**
* 测试fina的重排序规则
*/
public class FinalExample {
int i; // 普通变量
final int j; // final 变量
static FinalExample obj;
public FinalExample() {
i = 1;
j = 2;
}
public static void wirter(){
obj = new FinalExample();
}
public static void reader(){
FinalExample object = obj;
int a = object.i;
int b = object.j;
System.out.println(a);
System.out.println(b);
}
public static void main(String[] args) {
Thread a = new Thread(new Runnable() {
@Override
public void run() {
FinalExample.wirter();
}
});
Thread b = new Thread(new Runnable() {
@Override
public void run() {
FinalExample.reader();
}
});
// final一定是在构造函数中加载,不会被重排序,但是普通的就不一定了
// 可以吧ij在构造函数中,初始化的顺序换一下
// 就可以知道了
a.start();
b.start();
}
}
上面的代码,是对这个final重排序的验证,如果按照上面的顺序来的话,输出的结果是1,2,是符合预期的,但是你把构造函数中的i和j的位置呼唤下,这个结果就不会符合预期了。
1.jmm禁止编译器把final域的写重排序到构造函数之外
2.编译器会在final域的写之后,构造函数return之前,插入一个StoreStore屏障。这个屏障禁止处理器把final域写重排序到构造函数之外。
读final域的重排序规则是,在一个线程中,初次读对象引用与初次读该对象包含的final域,jmm禁止处理器重排序这两个操作。编译器会在读final域操作前面插入一个LoadLoad屏障。
public class FinalReferenceExample {
final int[] intArray;
static FinalReferenceExample obj;
public FinalReferenceExample() throws InterruptedException {
intArray = new int[1];
Thread.sleep(5);
intArray[0] = 1;
}
public static void writerOne() throws InterruptedException {
obj = new FinalReferenceExample();
}
public static void writerTwo() throws InterruptedException {
Thread.sleep(5);
obj.intArray[0] = 2;
}
public static void reader(){
if(obj != null){
int templ = obj.intArray[0];
System.out.println(templ);
}
}
public static void main(String[] args) {
Thread a = new Thread(new Runnable() {
@Override
public void run() {
try {
FinalReferenceExample.writerOne();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread b = new Thread(new Runnable() {
@Override
public void run() {
try {
FinalReferenceExample.writerTwo();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
});
Thread c = new Thread(new Runnable() {
@Override
public void run() {
FinalReferenceExample.reader();
}
});
// 请况很难被模拟出来,所以我就加了这个sleep
a.start();
b.start();
c.start();
}
}
这个很难模拟出出异常的读不到对象引用的情况,但是加上sleep就可以。主要是为了确保第一次读是初始化之前读。
构造函数内对一个final引用的对象成员域的写入,与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序。
/**
* 测试溢出
*/
public class FinalReferenceEscapeExample {
final int i;
static FinalReferenceEscapeExample obj;
// 构造函数返回之前,被构造对象的引用不能被其他对象可见,不然就会出现溢出
public FinalReferenceEscapeExample(){
i = 1;
obj = this; // 放到前面就不会溢出了,为什么溢出呢,因为指令重排序。这个可能会在构造函数后面执行,类似第一个i一样
}
public static void wirter(){
new FinalReferenceEscapeExample();
}
public static void reader(){
if ( obj != null){
int temp = obj.i;
System.out.println(temp);
}
}
public static void main(String[] args) {
Thread a = new Thread(new Runnable() {
@Override
public void run() {
FinalExample.wirter();
}
});
Thread b = new Thread(new Runnable() {
@Override
public void run() {
FinalExample.reader();
}
});
// 异常是对的,i说明没被初始化
a.start();
b.start();
}
}
为什么不能溢出,就是这个对象的final域一定是在初始化之前完成,而且在final初始化完成之前,这个构造对象是不能被其他线程看见。所以不会导致溢出。之前介绍volatile说到过构造函数的溢出。但是当时没有详细的介绍。这里我就详细的说下,所谓的构造函数的溢出,就是对象初始化完成了,当时构造函数的赋值还没完成。这个时候对象已经不是空的了,但是属性值依然为空,这个时候通过这个对象引用去读这个属性值。就会出现异常或者读出的属性值为空的情况。这个就叫从构造函数中溢出。
final的语义大致就是,包含final 的变量和引用的对象,在final变量和引用完成之前,是不能构造出对象,且不能被其他线程所见的。为什么要这样呢,防止fianl的值被改变(如果初始化完成之前你读到fina的int变量可能值为零,初始化完成之后,你再读到可能为1,就不能保证final值不可改变的定义了)。