共享对象

编写正确的并发程序关键在于对共享的、可变状态进行访问管理。

可以使用同步来避免多个线程在同一时间访问同一数据;也可以使用共享和发布对象技术,使多个线程可以安全地访问到它们。

这一篇主要侧重于第二种方法。

发布和逸出

发布一个对象是使它能够被当前范围之外的代码所使用,比如将一个引用存储到其他代码可以访问的地方。

如果发布对象时,它还没有完成构造,同样危及线程安全,一个对象在尚未准备好时就就将他发布,这种情况叫逸出。

下面分别用两个例子来演示对象的发布和逸出。

code1

class UnsafeStates {
    private String[] states = new String[] {
        "ak","al",....
    };
    public String[] getStates() {return states;}
}

在上面的例子中,这个类发布了本来是私有的数组引用 states, 任何一个调用者都能修改它的内容。

code2

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逸出,所谓逸出,就是在不该发布的时候发布了一个引用。在这个例子里面,当我们实例化ThisEscape对象时,会调用source的registerListener方法,这时便启动了一个线程,而且这个线程持有了ThisEscape对象(调用了对象的doSomething方法),但此时ThisEscape对象却没有实例化完成(还没有返回一个引用),所以我们说,此时造成了一个this引用逸出,即还没有完成的实例化ThisEscape对象的动作,却已经暴露了对象的引用。其他线程访问还没有构造好的对象,可能会造成意料不到的问题。

正确的构造方法

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;
  }

  void doSomething(Event e) {
  }

  interface EventSource {
    void registerListener(EventListener e);
  }

  interface EventListener {
    void onEvent(Event e);
  }

  interface Event {
  }
 }

在这个构造中,我们看到的最大的一个区别就是:当构造好了SafeListener对象(通过构造器构造)之后,我们才启动了监听线程,也就确保了SafeListener对象是构造完成之后再使用的SafeListener对象。

对于这样的技术,书里面也有这样的注释:

具体来说,只有当构造函数返回时,this引用才应该从线程中逸出。构造函数可以将this引用保存到某个地方,只要其他线程不会在构造函数完成之前使用它。