对象的安全构造

在构造期间,不要公布“this”引用

一种可以将数据争用引入类中的错误是,在构造函数完成之前,使 this 引用暴露给另一个线程。有时这个引用是显式的,(譬如,直接将 this 存储在静态字段或集合),但还有一些时候它可以是隐式的(譬如,当将一个引用公布给构造函数中的非静态内部类的实例时)。构造函数不是一般的方法 ― 它们有特殊的用于初始化安全的语义。在构造函数完成之后,可以认为对象是处于一种可预测和一致的状态,将引用公布给一个还未完成构造的对象是危险的。清单 2 显示了将这类争用条件引入构造函数的示例。这个示例看上去可能没有危害性,但它可以引发严重的并发性问题。

//清单 2. 可能发生的数据争用
public class EventListener {
  public EventListener(EventSource eventSource) {
    // do our initialization
    ...
    // register ourselves with the event source
    eventSource.registerListener(this);
  }
  public onEvent(Event e) {
    // handle the event
  }
}
 

乍一看, EventListener 类似乎没有危害性。构造函数最后完成的工作是注册侦听器,这会将引用公布给新对象,这时其它线程可能会看到这个引用。但即使不考虑所有 Java 内存模型(JMM)问题(譬如,多个线程所看见同一数据的不一致性以及内存访问重排序的不同),该代码仍然有将还未完成构造的 EventListener 对象暴露给其它线程的危险。考虑清单 3 中这种情况,当创建 EventListener 的一个子类时,会发生什么:

//清单 3. 创建 EventListener 的一个子类时,问题产生了
public class RecordingEventListener extends EventListener {
  private final ArrayList list;
  public RecordingEventListener(EventSource eventSource) {
    super(eventSource);
    list = Collections.synchronizedList(new ArrayList());
  }
  public onEvent(Event e) {
    list.add(e);
    super.onEvent(e);
  }
  public Event[] getEvents() {
    return (Event[]) list.toArray(new Event[0]);
  }
}
 

由于 Java 语言规范要求对 super() 的调用应该是子类构造函数中的第一条语句,所以在完成子类字段的初始化之前,还未构造完的事件侦听器已经对事件源进行了注册。现在, list 字段存在数据争用。如果事件侦听器决定从注册调用内发送一个事件,或者我们很不幸,在这不恰当的时刻,一个事件到来了,则会调用 RecordingEventListener.onEvent() ,而这时 list 仍然是 null 值,结果会抛出 NullPointerException 异常。就象 onEvent() 这样的类方法一样,在编码时,应该避免使用还未初始化完的 final 字段。

 

清单 2 中的问题在于, EventListener 在构造完成之前会向正在构造的对象公布一个引用。虽然看上去 几乎已经完整地构造了对象,所以将 this 传递给事件源好象是安全的,但这种表像是具有欺骗性的。公布来自构造函数内的 this 引用(如清单 2 所示)就象放置了一颗随时会爆炸的定时炸 弹。

 

不要隐式地暴露“this”引用

在根本不使用 this 引用情况下,也有可能会造成逃脱的引用问题。非静态内部类维护着其父类的 this 引用的隐式副本,所以创建匿名的内部类实例,并将其传递给从当前线程外部可以看见的对象,会存在与暴露 this 引用本身所具有的所有相同的风险。考虑清单 4,它与清单 2 有同样的根本问题,但这里没有显式地使用 this 引用:

//清单 4. 使用匿名内部类时,不正确地公布了“this”
public class EventListener2 {
  public EventListener2(EventSource eventSource) {
    eventSource.registerListener(
      new EventListener() {
  public onEvent(Event e) {
          eventReceived(e);
        }
      });
  }
  public onEvent(Event e) {
  }
}
 

 

EventListener2 类和其类似代码 清单 2 中的 EventListener 有同样的弊端:公布了对正在构造的对象的引用 ― 在这种情况下,是间接的 ― 另一个线程可以看见这个引用。如果打算创建 EventListener2 的子类,将会碰到同样的问题,即在子类构造函数完成之前会调用子类方法。

 

不要从构造函数内启动线程

清单 4 问题的一个特例是,从构造函数内启动了一个线程,由于当一个对象拥有一个线程时,通常这个线程要么是一个内部类,要么我们将 this 引用传递给其构造函数(或者该类本身继承了 Thread 类)。如果一个对象将拥有一个线程,那么,如果对象如 Thread 那样提供一个 start() 方法,并从 start() 方法而不是从构造函数启动线程,那是最好的。虽然通过接口,这会暴露类的一些实现细节(譬如,可能存在拥有一个线程),这往往是我们不期望的,但在这种情况下,从构造函数启动线程的害处要比隐藏实现所带来的好处多。

“公布”是指什么?

 

在构造期间,不是所有对 this 引用的引用都是有害的,只有那些公布引用,使其它线程看到该引用的引用才是有害的。确定与其它对象共享 this 引用是否安全,需要详细了解对象的可见性以及对象将如何利用引用。关于让 this 引用在构造期间逃脱,最好的策略是,在构造函数中完全避免使用 this 引用(直接或间接)。然而,实际上这种完全避免使用 this 引用的可能并不总是存在。但只要记住,在构造函数中谨慎使用 this 引用和创建非静态内部类的实例。

 在构造期间不允许引用逃脱的其它理由

当我们考虑同步的影响时,上面所详细讲述的关于线程安全构造的那些做法就变得更为重要。例如,当线程 A 启动线程 B 时,Java 语言规范(Java Language Specification (JLS))保证:当线程 A 启动线程 B 时,所有那些对线程 A 可见的变量对线程 B 也是可见的,这非常类似 Thread.start() 中隐式同步。如果从构造函数中启动一个线程,并且正在构造的对象还没有完成,则我们会丢失这些可见性的保证。

因为 JMM 有一些令人迷惑的方面,Java Community Process JSR 133 正对其进行修订,这将(尤其)会更改 volatile 和 final 的语义,使它们更符合其字面上含义。例如,在旧的 JMM 语义下,一个线程在其生存期中可能看到 final 字段有多个值。新的内存模型语义将防止这种情形,但只有在正确定义了构造函数的情况下 ― 这意味着在构造期间不允许 this 引用逃脱。

 

结束语

对其它线程能够看见的还未完成构造的对象进行引用显然不是我们所期望的。归根结底,我们如何正确辨别完全构造好的对象和尚未构造好的对象呢?但通过公布来自构造函数内的 this 引用 ― 直接或间接地通过内部类 ― 我们这样做时,会导致无法预料的后果。为了防止这种危险,在创建内部类的实例或从构造函数启动线程时,尽量避免使用 this 。如果无法在构造函数中避免直接或间接使用 this ,则确保不让其它线程看见 this 引用。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值