书中3.2中关于this引用溢出例子:
隐式地使this引用逸出(不要这么做):
public class ThisEscape {
public ThisEscape(EventSource source){
source.registerListener(
new EventListener(){
public void onEvent(Event e){
doSomething()e;
}
});
}
}
这段代码的问题是对象逸出。
这将导致this逸出,所谓 逸出,就是在不该发布的时候发布了一个引用。
当我们声明一个非静态内部类时,编译器会添加对外部类的一个隐式引用。 即非静态的内部类会持有外部类的一个隐式引用。
在构造过程中使this引用逸出的一个常见错误是:在构造过程中启动一个线程。单对象在其构造函数中创建一个线程时,无论是显式创建(通过将它传给构造函数)还是隐式创建(由于Thread或Runnable是该对象的一个内部类),this引用都会被新创建的线程共享。在对象尚未完全构造之前,新的线程就可以看见它。
当我们实例化ThisEscape对象时,会调用source的registerListener方法,这时便启动了一个线程,而且这个线程持有了通过注册的内部类持有了外部ThisEscape对象引用,但此时ThisEscape对象却没有实例化完成(还没有返回一个引用),所以我们说,此时造成了一个this引用逸出,即还没有完成的实例化ThisEscape对象的动作,却已经暴露了对象的引用。其他线程访问还没有构造好的对象,可能会造成意料不到的问题。
以下是网友分享的解答,让我真正理解this逸出的理由(了解底层实现原理和学习看源码的重要性):
1 **内部类、匿名内部类都可以访问外部类的对象的域,为什么会这样,实际上是因为内部类构造的时候,会把外部类的对象this隐式的作为一个参数传递给内部类的构造方法,这个工作是编译器做的,**他会给你内部类所有的构造方法添加这个参数,所以你例子里的匿名内部类在你构造ThisEscape时就把ThisEscape创建的对象隐式的传给匿名内部类了。
2 最近在看《Java 并发编程实战》,个人的理解:
首先,看里面的 doSomething(e) 方法,这个方法应该是在 ThisEscape 中,不然就无法解释。也就是说,通过 doSomething(e) 方法可以修改 ThisEscape 中的属性或者调用 ThisEscape 中的其他方法。
例子中的代码,在多线程环境下,会出现这样一种情况:
线程 A 和线程 B 同时访问 ThisEscape 构造方法,这时线程 A 访问构造方法还为完成(可以理解为 ThisEscape 为初始化完全),此时由于 this 逸出,导致 this 在 A 和 B 中都具有可见性,线程 B 就可以通过 this 访问 doSomething(e) 方法,导致修改 ThisEscape 的属性。也就是在 ThisEscape 还为初始化完成,就被其他线程读取,导致出现一些奇怪的现象。
这也就是 this 逸出。
通过 《Java 并发编程实战》 官网的书本 example 源码包,也证实了 doSomething 的确是 ThisEscape 中的方法。
package net.jcip.examples;
/**
* ThisEscape
* <p/>
* Implicitly allowing the this reference to escape
*
* @author Brian Goetz and Tim Peierls
*/
public class ThisEscape {
public ThisEscape(EventSource source) {
source.registerListener(new EventListener() {
public void onEvent(Event e) {
doSomething(e);
}
});
}
void doSomething(Event e) {
}
interface EventSource {
void registerListener(EventListener e);
}
interface EventListener {
void onEvent(Event e);
}
interface Event {
}
}
书中给了解决方法,使用工厂方法防止this引用在构造过程中逸出。
public class SafeListener {
private final EventListener listener;
private SafeListener(){
listener = new EventListener(){
public void onEvent(Event e){
doSomething(e);
}
}
}
public static SafeListener newInstance(EventSource source){
SafeListener safe = new SafeListener();
source.registerListener(safe.listener);
return safe;
}
}
在这个构造中,我们看到的最大的一个区别就是:当构造好了SafeListener对象(通过构造器构造)之后,我们才启动了监听线程,也就确保了SafeListener对象是构造完成之后再使用的SafeListener对象。
不在构造方法中注册监听器,就不会创建持有外部引用的线程,this引用也就不会逸出。
对于这样的技术,书里面也有这样的注释:
具体来说,只有当构造函数返回时,this引用才应该从线程中逸出。构造函数可以将this引用保存到某个地方,只要其他线程不会在构造函数完成之前使用它。