到目前为止, 我们重点讨论的是如何确保对象不被发布, 例如让对象封闭在线程或另一个对象的内部。 当然,在某些情况下我们希望在多个线程间共享对象, 此时必须确保安全地进行共享。 然而, 如果只是像程序清单 3-14 那样将对象引用保存到公有域中, 那么还不足以安全地发布这个对象。
你可能会奇怪, 这个看似没有问题的示例何以会运行失败。由于存在可见性问题, 其他线程看到的Holder 对象将处于不一致的状态, 即便在该对象的构造函数中已经正确地构建了不变性条件。这种不正确的发布导致其他线程看到尚未创建完成的对象。
不正确性的发布
你不能指望一个尚未被完全创建的对象拥有完整性。某个观察该对象的线程将看到对象处于不一致的状态, 然后看到对象的状态突然发生变化, 即使线程在对象发布后还没有修改过它。 事实上, 如果程序清单3-15中的Holder使用程序清单3-14中的不安全发布方式, 那么另一个线程在调用assertSanity时将抛出AssertionError 。
由于没有使用同步来确保Holder对象对其他线程可见, 因此将Holder称为“ 未被正确发布”。在未被正确发布的对象中存在两个问题。首先, 除了发布对象的线程外, 其他线程可以看到的Holder域是一个失效值, 因此将看到一个空引用或者之前的旧值。然而, 更糟糕的情况是, 线程看到Holder引用的值是最新的, 但Holder状态的值却是失效的。情况变得更加不可预测的是, 某个线程在第一次读取域时得到失效值, 而再次读取这个域时会得到一个更新值,这也是assertSainty抛出AssertionError的原因。
如果没有足够的同步, 那么当在多个线程间共享数据时将发生一些非常奇怪的事情。
不可变对象与初始化安全性
由于不可变对象是一种非常重要的对象, 因此Java内存模型为不可变对象的共享提供一种特殊的初始化安全性保证。我们已经知道, 即使某个对象的引用对其他线程是可见的,也并不意味着对象状态对于使用该对象的线程来说一定是可见的。为了确保对象状态能呈现出一致的视图, 就必须使用同步。
另一方面,即使在发布不可变对象的引用时没有使用同步, 也仍然可以安全地访问该对象。为了维持这种初始化安全性的保证, 必须满足不可变性的所有需求:状态不可修改, 所有域都是final类型, 以及正确的构造过程。(如果程序清单3-15中的Holder对象是不可变的,那么即使Holder 没有被正确地发布, 在assertSanity中也不会抛出Asserti.onError。)
任何线程都可以在不需要额外同步的情况下安全地访问不可变对象,即使在发布这些对象时没有使用同步。
这种保证还将延伸到被正确创建对象中所有 final 类型的域。在没有额外同步的情况下,也可以安全地访问 final类型的域。然而,如果final 类型的域所指向的是可变对象,那么在访问这些域所指向的对象的状态时仍然需要同步。
安全发布的常用模式
可变对象必须通过安全的方式来发布,这通常意味若在发布和使用该对象的线程时都必须使用同步。现在,我们将重点介绍如何确保使用对象的线程能够看到该对象处于已发布的状态,并稍后介绍如何在对象发布后对其可见性进行修改。
要安全地发布一个对象,对象的引用以及对象的状态必须同时对其他线程可见。一个正确构造的对象可以通过以下方式来安全地发布:
a.在静态初始化函数中初始化一个对象引用。
b.将对象的引用保存到volatile 类型的域或者AtomicReferance 对象中。
c.将对象的引用保存到某个正确构造对象的final 类型域中。
d.将对象的引用保存到一个由锁保护的域中
在线程安全容器内部的同步意味着,在将对象放入到某个容器,例如Vector 或synchronizedList时,将满足上述最后一条需求。如果线程A将对象X放入一个线程安全的容器,随后线程B读取这个对象,那么可以确保B看到A设置的X 状态,即便在这段读/写X的应用程序代码中没有包含显式的同步。尽管Javadoc 在这个主题上没有给出很清晰的说明,但线程安全库中的容器类提供了以下的安全发布保证:
a.通过将一个键或者值放入Hashtable、synchronizedMap 或者ConcurrentMap 中,可以安全地将它发布给任何从这些容器中访问它的线程(无论是直接访问还是通过迭代器访问。
b.通过将某个元素放入Vector 、CopyOnWriteArrayList、CopyOnWriteArraySet 、synchronizedList或synchronizedSet 中,可以将该元素安全地发布到任何从这些容器中访问该元素的线程。
c.通过将某个元素放入BlockingQueue 或者ConcurrentLinkedQueue 中,可以将该元素安全地发布到任何从这些队列中访问该元素的线程。
类库中的其他数据传递机制(例如Future 和Exchanger) 同样能实现安全发布,在介绍这些机制时将讨论它们的安全发布功能。
通常,要发布一个静态构造的对象,最简单和最安全的方式是使用静态的初始化器:
public static Holder holder= new Holder(42);
静态初始化器由JVM在类的初始化阶段执行。由于在JVM内部存在着同步机制,因此通过这种方式初始化的任何对象都可以被安全地发布[JLS 12.4.2] 。
事实不可变对象
如果对象在发布后不会被修改, 那么对于其他在没有额外同步的情况下安全地访问这些对象的线程来说, 安全发布是足够的。所有的安全发布机制都能确保, 当对象的引用对所有访问该对象的线程可见时, 对象发布时的状态对于所有线程也将是可见的, 井且如果对象状态不会再改变, 那么就足以确保任何访问都是安全的。
如果对象从技术上来看是可变的, 但其状态在发布后不会再改变, 那么把这种对象称为“事实不可变对象(Effectively Immutable Object)"。这些对象不需要满足3.4节中提出的不可变性的严格定义。在这些对象发布后, 程序只需将它们视为不可变对象即可。通过使用事实不可变对象, 不仅可以简化开发过程, 而且还能由于减少了同步而提高性能。
在没有额外的同步的情况下,任何线程都可以安全地使用被安全发布的事实不可变对象。
可变对象
如果对象在构造后可以修改, 那么安全发布只能确保“发布当时” 状态的可见性。对于可变对象, 不仅在发布对象时需要使用同步, 而且在每次对象访问时同样需要使用同步来确保后续修改操作的可见性。要安全地共享可变对象, 这些对象就必须被安全地发布, 并且必须是线程安全的或者由某个锁保护起来。
安全地共享对象
当获得对象的一个引用时, 你需要知道在这个引用上可以执行哪些操作。在使用它之前是否需要获得一个锁?是否可以修改它的状态, 或者只能读取它?许多并发错误都是由于没有理解共享对象的这些“既定规则”而导致的。当发布一个对象时,必须明确地说明对象的访问方式。
网友评论