java并发编程实践 3.2 发布和逸出

缘起

《java并发编程实践》 3.2节

分析

对象的发布指的是一个对象能够被当前范围之外的代码使用. 例如

  1. 将一个引用存储到其他代码的可以访问的地方. 例如

    1
    2
    3
    4
    5
    public static Set<Secret> knownSecrets;

    public void initialize() {
    knownSecrets = new HashSet<>();
    }

    上面新建的HashSet的引用存储到静态域——knownSecrets中去了,而这个knownSecrets是可以被其他类或者线程访问的. 所以这个HashSet的引用就能够被3~5行这个小范围之外的代码所访问——可能并不在同一根线程中. 这就意味着我们将HashSet这个对象发布出去了.

  2. 在一个非私有方法中返回. 例如

    1
    2
    3
    4
    public Set getSecrets() {
    knownSecrets = new HashSet<>();
    return knownSecrets;
    }

    则HashSet这个对象就被调用上述getSecrets方法的代码(可能不在同一根线程中)获取到了,也就是HashSet这个对象的引用就被上述代码2~3行范围之外的代码使用了. 这就是说HashSet对象可以被当前范围之外的代码所访问. 也即该HashSet对象被发布出去了.

  3. 将此对象传递到别的类的方法中去了.

    1
    2
    3
    4
    public void() {
    knownSecrets = new HashSet<>();
    XXX.method(knownSecrets);
    }

    则第四行就将knownSecrets传递到了XXX的method范围去了. 这也属于将knownSecrets带入到了不属于当前范围内. 也就是将HashSet发布出去了.

对象的逸出指的是在对象尚未准备好的情况下就将其发布出去. 逸出是一种不安全的发布。

典型的例子是

1
2
3
4
5
6
7
8
9
10
public Class Escape {
public Escape(EventSource source) {
source.registerListener(new EventListener(){
public void onEvent(Event e) {
System.out.println(Escape.this); // 匿名内部类中可以访问Escape实例,注意,此时Escape实例尚未初始化完毕(因为构造器尚未执行完毕)
doSomething(e);
}
})
}
}

​ 源码1

为什么说这是逸出的例子呢? 因为Escape类的实例并没有准备好就通过匿名内部类——EventListener发布出去了. 举一个简单的例子:我们可以像下面在2根线程中分别调用下面的2行代码(即第一根线程A调用第一行代码,第二根线程B调用第二行代码)

1
2
new Escape(source);
source.getListeners();

注意,因为不是同一根线程,所以B调用getListeners的时候,A线程执行Escape未必构造器执行完毕了. 但是可以在source.getListeners() 中获取在Escape构造器中注册的EventListener(即B线程在”A尚未执行完Escape构造器,但是已经将EventListener实例注入了source中了”这个时机执行). 但是别忘了,这个EventListener是一个匿名内部类,它是可以通过Escape.this 访问到这个尚未创建完毕的Escape实例(它其实就是一个闭包). 所以这就是逸出。

鉴于上面的例子,我们就知道最好不要在类的构造器中写任何可能发布出去的匿名内部类. 例如启动一根线程这种事情千万做不得——在构造器中创建线程并没有错误,错误在于启动线程. 因为一旦启动线程就意味着我们可以在别的线程中访问到这个尚未初始化完毕的闭包实例了. 同样的道理,源码1中创建匿名内部类EventListener并没有错,错在你将该listener注册了. 因为注册就意味着发布出去了. 而该listener恰好可以访问到尚未实例化完成的Escape闭包。这就造成了尚未初始化完毕的Escape闭包的发布——也就是逸出.

那么怎么补救呢?很简单——不要在构造器中注册匿名内部类的Listener,同理,不要在构造器中启动Runnable线程.

以源码1为例子, 修改为

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public Class NoEscape {
private EventListener listener;

public Escape() {
listener = new EventListener(){
public void onEvent(Event e) {
System.out.println(Escape.this);
doSomething(e);
}
})
} // 创建,但是没注册, 这样就不会将尚未完成的NoEscape闭包逸出

public static void register(EventSource source) {
source.registerListener(listener);
}
}