1. 背景
2. 介绍
2.1 final修饰类、方法、变量的作用
- 修饰类:不可被继承;
- 修饰方法:不能被覆盖(重写);
- 修饰变量:一旦初始化,如果是基础类型,则不能被修改;如果是引用类型,则不能指向其它引用对象;
2.2 final重排序规则
- 在构造函数内对一个final域的写入,与随后把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序;
- 初次读一个包含final域的对象引用,与随后初次读这个final域,这两个操作之间不能重排序;
2.2.1 写final域的重排序规则
- JMM禁止编译器把final域的写重排序到构造函数之外;
- 编译器会在final域的写之后,构造函数return之前,插入一个StoreStore屏障。这个屏障禁止处理器把final域的写重排序到构造器之外,但是不会限制非final域,从而导致可能的线程问题。屏障的插入也是看处理器,例如X86处理器就不会对写-写重排序,因此会被省略掉(linux中uname -a可以看处理器是不是X86,目前大部分应该都是了)。
- 如果final域是一个引用对象,则在构造函数内对一个final引用的对象的成员域的写入,与随后在构造函数外把这个被构造对象的引用赋值给一个引用变量,这两个操作之间不能重排序;
写final域的重排序规则可以确保:在对象引用为任意线程可见之前,对象的final域已经被正确初始化过了,而普通域不具有这个保障。
2.2.2 读final域的重排序规则
- 在一个线程中,初次读对象引用与初次读该对象包含的final域,JMM禁止处理器重排序这两个操作(注意,这个规则仅仅针对处理器)。编译器会在读final域操作的前面插入一个LoadLoad屏障。同样如2.2.1所示,因处理器的不同而决定屏障是否插入(初次读对象引用与初次读该对象包含的final域,这两个操作之间存在间接依赖关系。由于编译器遵守间接依赖关系,因此编译器不会重排序这两个操作。大多数处理器也会遵守间接依赖,大多数处理器也不会重排序这两个操作。但有少数处理器允许对存在间接依赖关系的操作做重排序(比如alpha处理器),这个规则就是专门用来针对这种处理器);
读final域的重排序规则可以确保:在读一个对象的final域之前,一定会先读包含这个final域的对象的引用。在这个示例程序中,如果该引用不为null,那么引用对象的final域一定已经被A线程初始化过了。
2.3 final可见性
通过final域增加写和读重排序规则,只要对象是正确构造的(被构造对象的引用在构造函数中没有逸出),那么不需要使用同步(指lock和volatile的使用)就可以保证任意线程都能看到这个final域在构造线程中被初始化之后的值(之所以对其它线程可见,是因为StoreStore屏障刷新数据至主存,但是X86是省略了插入屏障,所以对其它线程不具备可见性了)。
2.3.1 发布
发布对象意味着该对象能在当前作用域之外的代码中被使用,比如,将类内部的对象传给其他类使用,或者一个非私有方法返回了该对象的引用等等。Java中强调类的封装性就是希望能合理的发布对象,保护类的内部信息。发布类内部状态,在多线程的环境下可能问题不大,但是在并发环境中却用可能严重地破坏多线程安全。
2.3.2 逸出
某个不该发布的对象被发布了,这种情况被称为逸出。
-
构造函数中将this逸出
- 内部的匿名类是隐私持有外部类的this引用的,这就无意中将this发布给内部类,如果内部类再被发布,则外部类就可能逸出,无意间造成内存泄漏和多线程安全问题。
public class ThisEscape3 {
private final int var;
public ThisEscape3(EventSource source) { //注册事件,会一直监听,当发生事件e时,会执行回调函数doSomething
source.registerListener( //匿名内部类实现
new EventListener() {
public void onEvent(Event e) { //此时ThisEscape3可能还未初始化完成,var可能还未被赋值,自然就发生严重错误
doSomething(e);
}
}
);
var = 10;
}
// 在回调函数中访问变量
int doSomething(Event e) {
return var;
}
}
3. 源码
4. 实战
5. FAQ
5.1 我发现部分人都习惯方法内局部变量、入参变量都修饰为final,这是有什么作用吗,比如提升性能、助于GC回收、线程安全?
5.2 屏障禁止两侧的指令重排序,这两侧是邻近的两条指令吗?
5.3 X86处理器限制了写-写重排序,那么会限制写普通域变量重排序到构造函数之外吗?另外既然X86省略了final构造函数内的StoreStore屏障,只是限制了写-写重排序,那么final域仍然不会重排序到构造函数之外了?
5.4 this的逸出导致了内存泄漏,是影响了GC吗?