《java并发编程实践》的第三章,对象的发布和逸出,作者提到了2种常见的对象逸出情况:在构造函数中注册事件监听,在构造函数中启动新线程。示例代码如下:
public class ThisEscape { public ThisEscape(EventSource source) { source.registerListener( new EventListener() { public void onEvent(Event e) { doSomething(e); } }); } }
public class ThreadThisEscape { public ThisEscape() { new Thread(new EscapeRunnable()).start(); // ... } private class EscapeRunnable implements Runnable { @Override public void run() { // ThreadThisEscape.this就可以引用外围类对象, 但是此时外围类对象可能还没有构造完成, 即发生了外围类的this引用的逃逸 } } }这2种方式的共同点是:在构造函数中使用内部类,并且代码可能会出现并行。也就是说,当内部类代码执行的时候,外部类对象的创建过程很有可能还没结束,这个时候如果内部类访问外部类中的数据,很有可能得到还没有正确初始化的数据。
stackoverflow上http://stackoverflow.com/questions/3705425/java-reference-escape讨论了this逸出的问题。下面给出2个具体点的例子,更容易理解this逸出的问题。
public class ThisEscape { private final int var; public ThisEscape(EventSource source) { source.registerListener( new EventListener() { public void onEvent(Event e) { doSomething(e); } } ); // more initialization // ... var = 10; } // result can be 0 or 10 int doSomething(Event e) { return var; } }
事件监听器一旦注册成功,就能够监听用户的操作,调用对应的回调函数。比如出发了e这个事件,会执行doSomething()回调函数。这个时候由于是异步的,ThisEscape对象的构造函数很有可能还没有执行完(没有给var赋值),此时doSomething中获取到的数据很有可能是0,这不符合预期。
public class ThreadThisEscape { private int weight = 0; public ThreadThisEscape() { weight = 1; new Thread(new EscapeRunnable()).start(); // 模拟构造函数耗时 for (int i = 0; i < 1000000; i++) { } } private class EscapeRunnable implements Runnable { @Override public void run() { System.out.println(ThreadThisEscape.this.weight); } } public static void main(String[] args) { new ThreadThisEscape(); } }