<span style="font-family: Arial, Helvetica, sans-serif; background-color: rgb(255, 255, 255);"><span style="white-space:pre"> </span>最近研读java并发编程,其中作者在提到对象的发布和逃逸的时候,讲到两种可能会发生this发布和逃逸的用法,但是单凭文章不能很好理解,所以,查阅相关资料做了相关实验,做一下记录,以作备忘。</span>
首先说明一下什么是this逃逸:this逃逸就是说,在构造函数返回之前,其他线程就已经取得了该对象的引用,由于构造函数还没有完成,所以,对象也可能是残缺的,所以,取得对象引用的线程使用残缺的对象极有可能发生错误的情况。因为这两个线程是异步的,取得对象引用的线程并不一定会等待构造对象的线程完结后在使用引用。
作者提到第一种就是在构造函数中注册一个监听器的时候,容易发生this逃逸。代码如下:
public class ThisEscape {
public ThisEscape(EventSource source) {
source.registerListener(
new EventListener() {
public void onEvent(Event e) {
doSomething(e);
}
});
}
}
上面的代码要理解起来有一些费劲,所以,我们做一个真实的实例,就比较好理解了。而且我们还可以得出两个很有用的结论。模拟项目代码代码如下:
片段一:监听器接口
public interface AEventListener {
public void onEvent(Object object);
}
片段二:监听器线程
public class ListenerRunnable implements Runnable {
private EventSource<AEventListener> source;
public ListenerRunnable(EventSource<AEventListener> source) {
this.source = source;
}
@Override
public void run() {
List<AEventListener> listeners = null;
try {
listeners = source.retrieveListeners();
} catch (InterruptedException e) {
e.printStackTrace();
}
for (AEventListener eventListener : listeners) {
eventListener.onEvent(new Object());
}
}
}
片段三:事件源
public class EventSource<T> {
private final List<T> eventListeners;
public EventSource() {
eventListeners = new ArrayList<T>();
}
public synchronized void registerListener(T eventListener) {
this.eventListeners.add(eventListener);
this.notifyAll();
}
public synchronized List<T> retrieveListeners() throws InterruptedException {
List<T> dest = null;
if (eventListeners.size() <= 0) {
this.wait(); //没有监听器注册到这里,就阻塞该线程。
}
dest = new ArrayList<T>(eventListeners.size());
dest.addAll(eventListeners);
return dest;
}
}
片段四:模拟this逃逸的类
public class ThisEscape {
public final int id;
public final String name;
public ThisEscape(EventSource<AEventListener> eventSource) {
id = 1;
eventSource.registerListener(
new AEventListener() {
@Override
public void onEvent(Object object) {
System.out.println("id:" + ThisEscape.this.id);
System.out.println("name:" + ThisEscape.this.name);
}
});
try {
TimeUnit.SECONDS.sleep(1);//此处加入相关断点。
} catch (InterruptedException e) {
e.printStackTrace();
}
name = "thisEscape test.";
}
}
片段五:测试代码
public class Main {
public static void main(String[] args) {
EventSource<AEventListener> eventSource = new EventSource<AEventListener>();
ListenerRunnable listenerRunnable = new ListenerRunnable(eventSource);
new Thread(listenerRunnable).start();
System.out.println();
ThisEscape escape = new ThisEscape(eventSource);
System.out.println(escape);
}
}
输出:
id:1
name:null
结论一:当注册的事件监听器监听到某一个事件发生时,会启用一个新的线程去执行相关的事情。(这个我们在代码中手动启动了一个线程来模拟,在没有监听器注册的时候,启动的线程会自动的阻塞!)
结论二:在外部内中初始化一个内部类的对象时,此内部类的对象保留了一个外部内对象的一个引用。这是java自带的一个机制,这在java中这种机制叫做“synthetic”。通过断点调试我们可以清晰的看到这种实现。
通过在代码片段四的TimeUnit.SECONDS.sleep(1);处打一个断点,来观察这个时候内存中的状态,我们发现结果是这样的,如下图所示:
我们发现,我们在程序中,EventSource中的一个列表eventListeners,本来按照泛型来说里面应该放的是AEventListener的对象,但是AEventListener是一个接口,所以不可以实例化,而且我们是在ThisEscape中实例化的一个AEventListener的一个匿名内部类,所以显示的类型就是“ThisEscape$1:表示这是一个在ThisEscape类中的一个匿名内部类,而且这个类一定是AEvenetListener的一个子类”。而且我们也发现,由于我们是在ThisEscape类中来实例化AEventListener的匿名类,所以实例化出来的对象里面包含一个指向外部类的对象的一个this引用,如箭头2所示,我们可以清楚的看到他的类型是和外部类的类型是一致的。
另外一种就是在在构造函数中启动新的线程的时候,容易发生This逃逸。代码如下:这一种理解起来就比较直观,所以,不做过多解释。
public class ThreadThisEscape {
public ThisEscape() {
new Thread(new EscapeRunnable()).start();
// ...
}
private class EscapeRunnable implements Runnable {
@Override
public void run() {
// ThreadThisEscape.this就可以引用外围类对象, 但是此时外围类对象可能还没有构造完成, 即发生了外围类的this引用的逃逸
}
}
}