Safe construction techniques

学习 Java 内存模型,查看相关文章:JSR 133 (Java Memory Model) FAQ,发现 final 字段的可见性依赖于正确的构造函数,里面给了一篇参考:Safe construction techniques,大致浏览一遍发现确实有很多之前没有思考到的内容。翻译成中文,加深理解。


主要内容:

  1. 引言
  2. 安全构造技术(Safe construction techniques
  3. 不要在构造期间发布 “this” 引用(Don't publish the "this" reference during construction
  4. 不要隐式暴露 "this" 引用(Don't implicitly expose the "this" reference
  5. 不要在构造器开启线程(Don't start threads from within constructors
  6. 你觉得 “发布” 是什么意思?(What do you mean by "publish"?
  7. 更多不要在构造期间泄露引用的原因(More reasons not to let references escape during construction
  8. 结论(Conclusion

引言

测试和调试多线程程序是极其苦难的,因为并发危险常常无法均匀或可靠的表现出来(because concurrency hazards often do not manifest themselves uniformly or reliably)。更多的线程问题本质上无法预测,在某些平台上(像单处理器系统)或者低于一定负荷之下可能都不会出现。正因为测试多处理器的正确性非常困难,而且 bug 可能很长时间才会出现,所以从一开始就开发线程安全的应用显得格外重要。本片文章中,我们想去探索一个特定的线程安全问题 – 允许 "this" 引用在构造期间泄露(我们称之为泄露引用(escaped reference)问题)– 会产生有些非常不期待的结果。接下来我们将建立线程安全构造器的一些指导原则。


安全构造技术

线程安全违规行为的程序分析是很困难的,需要专业经验。幸运的是,从一开始就创建线程安全的类并不是很困难,尽管它也需要一个专门的技巧:纪律(creating thread-safe classes from the outset is not as difficult, although it requires a different specialized skill: discipline)。大多数并发错误是由于程序员为了方便,认为会提高性能,或者仅仅是因为懒惰而违反规则造成的。类似于许多其它并发问题,在编写构造器时可以遵循一些简单的规则,就能避免泄露引用问题。

危险的竞争条件(Hazardous race conditions

大多数并发危险可以归结为几类数据竞争(data race)。数据竞争,或称为竞争条件,在多个线程或处理器同时读取一个共享数据项,而最终结果依赖于线程调度顺序的时候发生。代码片 1 给出了一个简单的数据竞争的例子,程序最终可能打印出 0 或者 1,这依赖于线程的调度。

public class DataRace {
  static int a = 0;

  public static void main() {
    new MyThread().start();
    a = 1;
  }

  public static class MyThread extends Thread {
    public void run() { 
      System.out.println(a);
    }
  }
}

如果第二个线程可以马上被调度,那么打印出的值为初始值 0;相反,如果第二个线程没有马上运行,那么结果为 1。这个程序的输出依赖于你使用的 JDK,底层操作系统的调度,或者随机时间影响(random timing artifacts)。多次运行可能会有多个不同结果。

可见性危险(Visibility hazards

在代码片 1 中,除了上面提到的明显的线程执行次序造成的数据竞争外,还有另一个数据竞争问题。第二个竞争是一个可见性竞争:两个线程没有使用同步,将无法确保线程对于数据改变的可见性(the second race is a visibility race: the two threads are not using synchronization, which would ensure visibility of data changes across threads.)。因为没有同步,所以第二个线程在第一个线程赋值操作完成之后运行,第一个线程做的数据改变可能无法马上对第二个线程可见。这种情况是可能的,就是尽管第一个线程已经赋值变量 a1,但是第二个线程仍旧看见变量 a 的值为 0。第二类数据竞争指两个线程在缺乏同步操作下访问同一个变量,不过幸运的是,当你读一个可能已经被另一个线程写过的变量,或者写一个将会被另一个线程读的变量的时候,使用同步操作可以避免这类数据竞争。更多关于此类数据竞争的内容可以查看下一小节 "Synching up with the Java Memory Model" 或者 相关主题

关于 Java 内存模型的同步(Synching up with Java Memory Model

Java 程序中的同步操作强制执行互斥(The keyword in Java programming enforces mutual exclusion,哪个 keyword?应该指同步操作):它确保一段时间内只有一个线程执行代码块。但是,如果缺少同步操作,将导致在弱内存模型(指平台不需要提供缓存一致性,platforms that don't necessarily provide cache coherency)的多处理器系统中出现更加微妙的结果(就是会出问题呗)。同步确保了一个线程完成的改变以可预测的方式对另一个线程可见。在某些体系结构下,在没有同步的情况下,不同的线程可能会看到内存操作看起来与实际执行的顺序不同。这令人困惑很正常,因为对于在这些平台上实现良好性能至关重要(指的是同步)。如果你遵循下面这个规则:

// 每一次你读一个可能已经被其他线程写入的变量;或者写入一个可能会被其它线程读的变量,使用同步操作 -- 你将不会有任何问题。
synchronize every time you read a variable that might have been written by another thread 
or write a variable that may be read next by another thread -- then you won't have any problems.

更多信息查看 相关主题


不要在构造期间发布 "this" 引用

其中一个会在类中造成数据竞争的错误是在构造器未完成之前将 "this" 引用暴露给了另一个线程(One of the mistakes that can introduce a data race into your class is to expose the this reference to another thread before the constructor has completed)。有时候这个引用是显式(explicit)的,比如将 "this" 引用存储到静态域(static field)或者集合(collection),但是有些时候它是隐式(implicit)的,比如当你在构造器中发布一个非静态内部类(non-static inner class)的实例引用时。构造器不是普通的方法 – 它们在初始化安全上有特殊语义。在构造器完成以后,对象被认为在一个可预测的,一致性的状态;在构造器未完成期间,将对象引用发布出去是危险的。代码片 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 看起来没有问题。构造器做的最后一件事情,是这个监听器的注册,这会将这个新对象的引用发布出去,这样其它线程可能会看到它(The registration of the listener, which publishes a reference to the new object where other threads might be able to see it, is the last thing that the constructor does)。即使忽略了所有 Java 内存模型(JMM)的问题,比如线程间可见性的差异和内存访问重排序,这段代码仍然存在危险,因为它暴露了一个不完全的 EventListener 构造对象给其它线程。考虑以下场景,当 EventListener 被继承的时候,比如代码片 3

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() 方法必须是第一个表达式(如果调用的话),所以在子类构造器未结束,子类字段未初始化之前,已完成了 EventListener 的注册。这样对于变量 list 会产生一个数据竞争。如果 EventListener 注册后立即调用了函数 onEvent,如果在错误的时间调用,那么 list 仍旧为 null,将抛出 NullPointerException 异常。类似于 onEvent 这样的类方法不应该写代码去判断 final 字段是否已经初始化。

代码片 2 的问题是 EventListener 在构造完成之前就发布了这个对象的引用。尽管看起来这个对象已经快要构造结束了,因此将 this 输入 EventSource 看起来是安全的,这是一种假象。像代码片 2 一样在构造器内发布 this 引用,是一枚等待爆炸的定时炸弹。


不要隐式暴露 "this" 引用

不使用 "this" 引用,也有可能出现泄露引用问题。非静态内部类保持了一个隐式的父类对象的 "this" 引用,所以在构造器中创建匿名内部类(anonymous inner class)实例,并将它传给其它线程可见的对象,这同样会有暴露 "this" 引用的风险。参考代码片 4,和代码片 2 一样有同样的问题,只是没有显式的 "this" 引用:

public class EventListener2 {
  public EventListener2(EventSource eventSource) {

    eventSource.registerListener(
      new EventListener() {
        public void onEvent(Event e) { 
          eventReceived(e);
        }
      });
  }

  public void eventReceived(Event e) {
  }
}

EventListener2 和类 EventListener 一样有相同的问题:本对象的引用在构造期间被发布出去了 – 本例子里不是直接的 – 那么其它线程就可以看见它。如果我们继承 EventListener2,会遇到同样的问题:子类构造器未完成就有子类方法被调用了。


不要在构造器开启线程

代码片 4 的问题的一个特殊情况是在构造器开启一个线程,因为经常当对象拥有一个线程,这个线程如果不是一个内部类,那么我们会把 "this" 引用输入到它的构造器(外部类,继承了 Thread)(either that thread is an inner class or we pass the this reference to its constructor(or the class itself extends the Thread class))。如果一个对象拥有一个线程,最好的方式是这个对象提供一个 start() 方法(就像 Thread 类一样),通过调用这个对象的 start() 方法来启动线程而不是在构造器内。尽管通过这个接口可能会暴露出一些实现细节(比如这个对象可能拥有一个线程),但是在这种情况下,在构造器启动线程的风险远超过隐藏实现细节的好处。


你觉得 “发布” 是什么意思?

不是所有在构造器中引用 "this" 的引用都是有害的,只有哪些将这个引用发布给其它线程的做法才是有害的。判断这个将 "this" 引用共享给另一个对象是否安全需要仔细的理解另一个对象的可见性以及它会用这个引用做些什么。代码片 5 包含了安全和不安全的例子,这些例子都在对象构造期间泄露的 "this" 引用:

public class Safe { 

  private Object me;
  private Set set = new HashSet();
  private Thread thread;

  public Safe() { 
    // Safe because "me" is not visible from any other thread
    // 安全因为 "me" 对其它线程不可见
    me = this;

    // Safe because "set" is not visible from any other thread
    // 安全因为 "set" 对其它线程不可见
    set.add(this);

    // Safe because MyThread won't start until construction is complete
    // and the constructor doesn't publish the reference
    // 安全因为 MyThread 没有在构造期间开始,构造器不会发布这个引用
    thread = new MyThread(this);
  }

  public void start() {
    thread.start();
  }

  private class MyThread(Object o) {
    private Object theObject;

    public MyThread(Object o) { 
      this.theObject = o;
    }

    ...
  }
}

public class Unsafe {
  public static Unsafe anInstance;
  public static Set set = new HashSet();
  private Set mySet = new HashSet();

  public Unsafe() {
    // Unsafe because anInstance is globally visible
    // 不安全因为 anInstance 是全局可见的
    anInstance = this;

    // Unsafe because SomeOtherClass.anInstance is globally visible
    // 不安全因为 SomeOtherClass.anInstance 是全局可见的
    SomeOtherClass.anInstance = this;

    // Unsafe because SomeOtherClass might save the "this" reference
    // where another thread could see it
    // 不安全因为 SomeOtherClass 可能会保存这个 "this" 引用,这样其它线程就会看到它
    SomeOtherClass.registerObject(this);

    // Unsafe because set is globally visible 
    // 不安全因为 set 是全局可见的
    set.add(this);

    // Unsafe because we are publishing a reference to mySet
    // 不安全因为 mySet 中包含了 "this" 引用
    mySet.add(this);
    SomeOtherClass.someMethod(mySet);

    // Unsafe because the "this" object will be visible from the new
    // thread before the constructor completes
    // 不安全因为 "this" 对象将在构造器结束之前就对其它线程可见
    thread = new MyThread(this);
    thread.start();
  }

  public Unsafe(Collection c) {
    // Unsafe because "c" may be visible from other threads
    // 不安全因为 "c" 可能对其它线程可见
    c.add(this);
  }
}

正如你所看到的,不安全类中的许多不安全结构与安全类中的安全结构具有显著的相似性。判断是否 "this" 引用可以对其它线程可见是非常复杂的。最好的策略是避免在构造器中使用 "this" 引用(直接或间接)。事实上,这也并不总是能够实现的。请记住小心使用 "this" 引用,以及在构造器中创建非静态内部类实例。


更多不要在构造期间泄露引用的原因

当我们考虑同步的影响时,上面详细介绍的线程安全构造器尤为重要。比如,当线程 A 启动线程 BJava 语言规定(JLS)保证了在线程 B 启动时对线程 A 可见的变量对线程 B 也同样可见,这就像在 Thread.start() 中有一个 隐式同步。如果在构造器中启动线程,那么构造器中的对象还没有构造完全,那么我们也会失去这部分的可见性保证(If we start a thread from within a constructor, the object under constructin is not completely constructed, and so we lose these visibility guarantees.)。

因为存在一些困惑的部分,JMM 被重新修订了,发布为 JSR 133。它改变了 volatilefinal 的语义,使它们更符合一般直觉。比如,在当前 JMM 语义下,一个线程可能会看见 final 字段在其生命周期内不止有一个值。新的内存模型语义避免了这一点,但是只有在构造函数正确定义的情况下 – 这意味着不要让 "this" 引用在构造期间泄露。


结论

将一个不完全构造对象的引用对其它线程可见的做法非常不可取。毕竟,这样做的话我们没法区分对象是否构造完成。但是如果在构造器内发布引用 – 直接使用 "this" 或者间接通过内部类 – 如果这样做,就有可能出现意料之外的结果。为了避免这一个危害,尽量不要再构造器中使用 “this”,创建内部类实例,或者启动线程。如果无法避免在构造器中使用 "this" 引用,那么确保 "this" 引用不会对其它线程可见。

个人见解

上面说明了在构造器中将 "this" 引用以直接或间接的方式泄露给其它线程的危害,提出几种避免引用泄露的方式:

  1. 不要在构造器中使用 "this" 引用。如果无法避免,确保它不会对其它线程可见;
  2. 不要在构造器中创建非静态内部类;
  3. 不要再构造器中启动线程。
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值