安全发布

发布与逸出

发布(Publish)”一个对象的意思是指:使对象能够在当前作用域之外的代码中使用当某个不应该发布的对象被发布时,这种情况就被称为逸出。

当发布一个对象时,在该对象的非私有域中引用的所有对象同样会被发布。

安全的对象构造过程

当且仅当对象的构造函数返回时,对象才处于可预测的和一致的状态。因此,当从对象的构造函数中发布对象时,只是发布了一个尚未构造完成的对象,即使发布对象的语句位于构造函数的最后一行也是如此。

不要在构造函数中使 this 引用逸出。

  • 在构造函数中调用一个可改写的实例方法时(既不是私有方法,也不是 final 方法),同样会导致 this 引用在构造过程中逸出。

  • 在构造函数中创建线程并没有错误,但最好不要立即启动它,而是通过一个 start 或 initialize 方法来启动。

实例化一个对象其实可以分为三个步骤:

  1. 分配内存空间。

  2. 初始化对象。

  3. 将内存空间的地址赋值给对应的引用。

由于操作系统可以对指令进行重排序,所以上面的过程也可能会变成如下过程:

  1. 分配内存空间。

  2. 将内存空间的地址赋值给对应的引用。

  3. 初始化对象

如果是这个流程,多线程环境下就可能将一个未初始化的对象引用暴露出来,从而导致不可预料的结果。

不可变对象与初始化安全性

任何线程都可以在不需要额外同步的情况下安全地访问不可变对象,即使在发布这些对象时没有使用同步。

  • 这种保证还将延伸到被正确创建对象中所有 final 类型的域。在没有额外同步的情况下,也可以安全地访问 final 类型的域。

  • 然而,如果 final 类型的域所指向的是可变对象,那么在访问这些所指向的对象的状态时仍然需要同步。

写 final 域重排序规则:

编译器会在 final 域写之后,构造函数 return 之前,插入一个 storestore 屏障。这个屏障可以禁止处理器把 final 域的写重排序到构造函数之外。

写 final 域的重排序规则可以确保:在对象引用为任意线程可见之前,对象的 final 域已经被正确初始化过了,而普通域就不具有这个保障。

读 final 域重排序规则:

处理器会在读 final 域操作的前面插入一个 LoadLoad 屏障。这个屏障可以禁止“初次读对象引用”和“初次读该对象包含的 final 域”这两个操作的重排序。

读 final 域的重排序规则可以确保:在读一个对象的 final 域之前,一定会先读包含这个final 域的对象的引用。

安全发布的常用模式

要安全地发布一个对象,对象的引用以及对象的状态必须同时对其他线程可见

一个正确构造的对象可以通过以下方式来安全地发布:

  • 在静态初始化函数中初始化一个对象引用

  • 将对象的引用保存到 volatile 类型的域,或者 AtomicReference 对象中

  • 将对象的引用保存到某个正确构造对象的 final 类型域中

  • 将对象的引用保存到一个由锁保护的域中

所有的安全发布机制都能确保,当对象的引用对所有访问该对象的线程可见时,对象发布时的状态对于所有线程也将是可见的。

对象的发布需求取决于它的可变性:

  • 不可变对象可以通过任意机制来发布

  • 事实不可变对象必须通过安全发布方式来发布

  • 可变对象必须通过安全发布方式来发布,并且必须是线程安全的或者由某个锁保护起来

事实不可变对象

如果对象从技术上来看是可变的,但其状态在发布后不会再改变,那么把这种对象称为“事实不可变对象(Effectively Immutable Object)”

在没有额外同步的情况下,任何线程都可以安全地使用被安全发布的事实不可变对象。

对于可变对象,安全发布只能确保“发布当时”状态的可见性。

因此,对于可变对象,不仅在发布对象时需要使用同步,在每次对象访问时同样需要使用同步来确保后续修改操作的可见性。

最后更新于