`

对象的安全构造

    博客分类:
  • Java
 
阅读更多

在构造期间,不要公布“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 引用。

分享到:
评论

相关推荐

    面向对象软件构造(内含57个参考文献)

    面向对象软件构造是软件开发领域中的一个重要概念,它基于面向对象编程(Object-Oriented Programming,简称OOP)的理念,通过模拟现实世界中的实体和它们之间的关系来构建复杂的软件系统。这种方法使得代码更加模块...

    面向对象软件构造

    面向对象软件构造是软件开发领域中的一个重要概念,它基于面向对象编程(Object-Oriented Programming,简称OOP)思想,旨在提高代码的可维护性、重用性和扩展性。本教程将深入探讨这一主题,帮助你理解并掌握面向...

    面向对象软件构造(第二版)中英对照版

    类是对象的蓝图,对象是类的实例,封装保证了数据的安全性,继承提供了代码的复用性,而多态性允许不同的对象对同一消息作出不同的响应。 2. **统一建模语言(UML)**:书中介绍了UML作为面向对象设计的主要工具,...

    面向对象软件构造(Object-Oriented Software Construction)(已补上33章)

    4. 封装(Encapsulation):隐藏对象的内部实现细节,只暴露必要的接口供外部使用,保护数据的安全性。 5. 多态(Polymorphism):不同对象对同一消息可以有不同的响应,增强了代码的灵活性和可扩展性。 6. 接口...

    面向对象软件构造(第二版)1-6章

    面向对象软件构造(第二版)1-6章的知识点概览与深度解析 一、软件品质与外部因素 《面向对象软件构造》第二版的前六章深入探讨了软件工程的核心概念,尤其聚焦于软件质量及其受外部和内部因素的影响(第1章)。软件...

    讲解Java中如何构造内部类对象以及访问对象

    本篇将主要讨论如何构造内部类对象以及如何通过反射来访问它们。 首先,我们来看一个简单的内部类示例: ```java package javalang; public class Outer { public static class Inner1 { // ... } } ``` 在这...

    面向对象软件构造-delphi技术资料

    面向对象软件构造是一种现代软件开发方法,它基于面向对象编程(OOP)的概念,强调将现实世界中的问题抽象为对象,通过对象之间的交互来解决问题。Delphi是一款强大的集成开发环境(IDE),它以其高效的编译器和VCL...

    建筑构造的研究对象与方法.pptx

    《建筑构造的研究对象与方法》 建筑构造是建筑工程领域中不可或缺的一部分,它主要研究建筑物各个组成部分的构造原理和构造方法。这一学科具有很强的实践性和综合性,它的主要任务是研究并提供适用、安全、经济、...

    面向对象软件构造object-oriented software construction

    这种封装提高了代码的安全性,因为内部细节对外部隐藏,只暴露必要的接口供其他对象交互。 2. 继承:继承允许子类从父类继承属性和行为,减少了代码重复,提高了代码的复用性。子类可以扩展或修改父类的行为,从而...

    面向对象软件构造-Delphi技术资料

    3. **封装**: 封装是隐藏对象的内部细节,只暴露必要的接口给外部使用,以保护数据安全和简化编程。Delphi提供了属性(properties)、方法(methods)和事件(events)来实现封装。 4. **VCL框架**: VCL是Delphi的...

    PHP的类、对象、构造方法.ppt

    - `private`属性仅限于类内部访问,有助于防止外部代码直接修改对象状态,从而确保数据安全和封装性。 - 数据隐藏是OOP的重要原则,通过控制访问权限,可以更好地控制程序的行为和防止意外修改。 7. **属性的...

    面向对象软件构造-Delphi技术资料 (电子书)

    2. **封装**:隐藏对象的内部细节,只对外提供公共接口,以保护数据的安全性。Delphi通过访问修饰符(如`private`,`protected`,`public`)实现封装。 3. **多态**:同一消息可以被不同的对象以不同的方式响应。...

    基于泛型的,高性能的,可指定构造函数及传入参数初始化的,线程安全的,扩展性非常高的传说中的对象池

    基于泛型的,高性能的,可指定构造函数及传入参数初始化的,线程安全的,扩展性非常高的传说中的对象池 详细看: http://blog.csdn.net/luyikk/archive/2010/05/10/5576550.aspx

    day07【类与对象、封装、构造方法】.pdf

    类与对象、封装、构造方法 类和对象是面向对象编程的基础概念,它们之间的关系是面向对象编程的核心。类是对一类事物的描述,是抽象的;对象是一类事物的实例,是具体的。类是对象的模板,对象是类的实体。 面向...

    面向对象设计与构造PPT

    面向对象设计与构造是计算机科学领域中的核心课程,特别是在Java编程语言的学习中占有重要地位。本课程涵盖了多个关键知识点,旨在帮助学生理解并掌握面向对象编程的基本原理和实践技巧。 首先,从标题“面向对象...

    dotnet C# 如果在构造函数抛出异常 是否可以拿到对象赋值的变量.rar

    标题中的“dotnet C# 如果在构造函数抛出异常 是否可以拿到对象赋值的变量”是一个关于C#编程语言中的构造函数和异常处理的问题。在C#中,构造函数是用于初始化新创建的对象的特殊方法。当在构造函数中抛出异常时,...

    06【类与对象、封装、构造方法】1

    this关键字用于在类的内部引用当前对象,它可以用来区分成员变量和局部变量,也可以在构造方法中引用其他构造方法,或者在方法中调用本类的其他方法。 【面向对象的三大特性】 除了封装之外,面向对象还有另外两个...

    C++复制构造函数详解

    在C++编程语言中,复制构造函数是一种特殊的构造函数,它的主要作用是创建一个对象的新副本。当一个已存在的对象被用作创建新对象的初始值时,复制构造函数会被调用。理解复制构造函数的工作原理及其应用是C++程序员...

    不要轻视拷贝构造函数与赋值函数

    - 定义:拷贝构造函数是一种特殊的构造函数,用于根据已有对象创建新对象。 - 调用时机:当创建新对象时,如果该对象的初始化需要一个同类型对象作为参数,则会调用拷贝构造函数。 - 示例:`String c(a);` 2. **...

Global site tag (gtag.js) - Google Analytics