`

对象的安全构造

    博客分类:
  • 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 引用。

分享到:
评论

相关推荐

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

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

    象山煤矿中小型地质构造发育规律及其预测

    通过对象山井田勘探、建井及采掘生产以来的地质构造资料进行整理、统计和作图分析,系统总结了已采区断层构造的发育特征和展布组合规律,并对井田未采区的地质构造进行了预测。结果显示,象山煤矿煤层中的断层构造极为...

    构造煤地球物理响应特征及成因分析——以晋城矿区赵庄煤矿为例

    研究构造煤的分布规律对煤层气的勘探开发及煤矿的安全生产有非常重要的意义。以晋城矿区赵庄煤矿为研究对象,首先通过分析含煤地层测井响应特征,圈定构造煤垂向展布;然后通过波阻抗反演圈定构造煤的横向展布;最后结合...

    js面向对象之常见创建对象的几种方式(工厂模式、构造函数模式、原型模式)

    虽然使用 Object构造函数 或者使用 对象字面量 可以很方便的用来创建一个对象,但这种方式有一个明显的缺点:使用一个接口创建多个对象会产生很多冗余的代码。因此为了解决这个问题,人们开始使用以下几种方式来常见...

    Java面向对象程序设计方案练习题(1).doc

    为MyTime类定义构造方法 以方便创建对象时初始化成员变量。 再定义diaplay方法 用于将时间信息打印出来。 为MyTime类添加以下方法 addSecond(int sec) addMinute(int min) addHour(int hou) subSecond(int sec) ...

    深度探索模C++对象模型PDF

    6.1 对象的构造和解构(Object Construction and Destruction) 全局对象(Global Objects) 局部静态对象(Local Static Objects) 对象数组(Array of Objects) Default Constructors和数组 6.2 new和delete...

    深度探索C++对象模型 超清版

    6.1 对象的构造和解构(Object Construction and Destruction) 全局对象(Global Objects) 局部静态对象(Local Static Objects) 对象数组(Array of Objects) Default Constructors和数组 6.2 new和delete...

    论文研究-面向对象的铁路超限货物轮廓计算方法.pdf

    将超限货物视为独立对象,结合对象特征构造以基本体素分解和几何运算相结合的实体对象描述方法;投影对象后以纵横交替扫描方法对投影进行预处理;通过定义交点序列、内外边等概念,设计了以交点确定和边属性判断为...

    平顶山矿区近构造区域煤与瓦斯突出规律研究

    以平顶山矿区发生的突出事故作为研究对象,从构造的影响对其进行研究,并对其发生突出原因进行了分析。研究结果表明:工作面从断层的下降盘向上升盘方向推进时更容易突出,通道形态是影响煤与瓦斯突出发生的主要原因,...

    Java并发编程规则:不可变对象永远是线程安全的.docx

    创建后状态不能被修改的对象叫作不可变对象。不可变对象天生就是线程安全的。它们的常量(变量)是在构造函数中创建的,既然它们的状态无法被修改,那么这些常量永远不会被改变——不可变对象永远是线程安全的。

    【05-面向对象(下)】

    抽象类里可以包含构造器,抽象类里的构造器并不是用于创建对象,而让其子类调用这些构 造器来完成属于抽象类的初始化操作。 •接口里不能包含初始化块,但抽象类则完全可以包含初始化块。 •一个类最多只能有一个...

    超级有影响力霸气的Java面试题大全文档

    wait是Object类的方法,对此对象调用wait方法导致本线程放弃对象锁,进入等待此对象的等待锁定池,只有针对此对象发出notify方法(或notifyAll)后本线程才进入对象锁定池准备获得对象锁进入运行状态。 17、...

    面向对象案例.zip

    python的pickle实战代码 pickle模块实现了用于序列化和反序列化Python对象结构的二进制协议。 “Pickling”是将Python对象层次结构转换为字节流的过程, ...pickle模块对于错误或恶意构造的数据是不安全的。

    矿工不安全行为影响因素分析及控制对策

    引入网络层次分析法(ANP),构造不安全行为影响因素的网络结构模型,并通过发放专家打分表,对不安全行为的影响因素进行两两比较,利用Super Decisions(SD)软件计算出不安全行为影响因素的一级因素和二级因素的权重,进而...

    义马跃进矿断层构造下冲击地压监测及防治

    以义马跃进矿25110工作面下巷为研究对象,通过对地质构造和断层情况的研究,运用综合指数法进行冲击危险程度评价和等级划分,并采用KJ550冲击地压在线监测系统实时监测受断层影响区域的应力集中情况,结合相应的卸压和...

    C++ 面向对象教程 21 courses#

    第1章 对象的演化 1 1.1 基本概念 1 1.1.1 对象:特性+行为 1 1.1.2 继承:类型关系 1 1.1.3 多态性 2 1.1.4 操作概念:OOP程序像什么 3 1.2 为什么C++会成功 3 1.2.1 较好的C 3 1.2.2 采用渐进的学习方式 4 1.2.3 ...

    《深度探索C++对象模型》(Stanley B·Lippman[美] 著,侯捷 译)

    6.1 对象的构造和解构(Object Construction and Destruction) 全局对象(Global Objects) 局部静态对象(Local Static Objects) 对象数组(Array of Objects) Default Constructors和数组 6.2 new和delete...

    论文研究-基于PLAZA的移动对象轨迹实时化简方法.pdf

    为了设计更为简洁高效的实时轨迹...通过融合区域过滤的思想,构造限制插值点误差范围的安全区域,设计出了针对移动对象实时位置信息进行化简的新算法NPLAZA。实验结果证明,算法 NPLAZA误差小,轨迹化简高效、稳定。

    自定义控件和手机安全助手

    custom工程演示了App开发的自定义控件相关知识,包括:自定义视图的步骤(声明属性、构造对象、测量尺寸、绘制视图)、自定义简单动画(任务片段、下拉刷新动画、圆弧进度动画)、自定义对话框的操作(对话框、改进...

    多构造软岩工作面冒顶治理工程实践

    为解决软岩回采工作面前方构造多容易冒顶的技术难题,以文家坝煤矿110603回采工作面为研究对象,采用现场调查及资料分析的研究手段,得出工作面发生冒顶主要原因:一是工作面前方出现大量断层构造导致煤体整体较为破碎,...

Global site tag (gtag.js) - Google Analytics