ITEM 82: 使用文档记录线程安全

ITEM 82: DOCUMENT THREAD SAFETY
  当类的方法被并发使用时,类的行为是它与客户端的契约的一个重要部分。如果您没有记录类的这方面行为,它的用户将被迫做出假设。如果这些假设是错误的,结果程序可能执行不充分的同步(item 78) 或过度同步(item 79)。无论哪种情况,都可能导致严重的错误。
  您可能听说过,可以通过在一个方法的文档中查找 synchronized 修饰符来判断该方法是否是线程安全的。这在几个方面是错误的。在正常的操作中,Javadoc 在其输出中不包含同步修饰符,这是有充分理由的。同步修饰符在方法声明中的出现是一个实现细节,而不是其 API 的一部分。它不能可靠地表示方法是线程安全的。
  此外,synchronized 修饰符的存在足以说明线程安全性,这种说法体现了一种误解,即线程安全性是一个全有或全无的属性。实际上,线程安全有几个级别。要启用安全的并发使用,类必须清楚地记录它支持的线程安全级别。下面的列表总结了线程安全级别。本报告并非详尽无遗,但涵盖常见个案:
  • 不变的 —— 这个类的实例看起来是不变的。不需要外部同步。示例包括String、Long和BigInteger (item 17)。
  • 无条件线程安全的类的实例是可变的,但是类有足够的内部同步,它的实例可以并发使用而不需要任何外部同步。示例包括 AtomicLong 和 ConcurrentHashMap。
  • 有条件线程安全 —— 类似无条件线程安全,除了一些方法需要外部同步来安全并发使用。示例包括 Collections.synchronized ,其迭代器需要外部同步。
  • 非线程安全的 —— 实例是可变的。要并发地使用它们,客户端必须将每个方法调用(或调用序列)与客户端选择的外部同步放在一起。示例包括通用的集合实现,比如ArrayList 和 HashMap。
  • 线程不安全的 —— 这个类对于并发使用是不安全的,即使每个方法调用都被外部同步包围。线程敌意通常是由于修改静态数据而没有同步造成的。没有人是故意编写线程敌对类的;此类类通常是由于没有考虑并发性而产生的。当一个类或方法被发现是线程不相容的,它通常是固定的或不赞成使用的。
  如果没有内部同步,item 78 中的 generateSerialNumber 方法将是线程不安全的,如322页所讨论的。
  这些类别(除了线程不安全的之外) 大致对应于 Java 并发性中的线程安全注释,它们是 不可变的、ThreadSafe 和 NotThreadSafe [Goetz06,附录A]。上述分类法中的无条件线程安全和有条件线程安全类别都在 ThreadSafe 注释中涉及。
  记录一个有条件线程安全的类需要小心。您必须指出哪些调用序列需要外部同步,以及必须获取哪些锁(在极少数情况下是锁)才能执行这些序列。通常是实例本身上的锁,但也有例外。例如,Collections.synchronizedMap 的文档说:
  当用户在其集合视图上迭代时,必须手动同步返回的映射:

Map m = Collections.synchronizedMap(new HashMap<>()); 
Set s = m.keySet(); // Needn't be in synchronized block
...
synchronized(m) { 
// Synchronizing on m, not s!
    for (K key : s) 
        key.f();
}

  如果不遵循此建议,可能会导致不确定性行为。
  类的线程安全描述通常属于类的文档注释,但是具有特殊线程安全属性的方法应该在它们自己的文档注释中描述这些属性。没有必要记录枚举类型的不变性。除非从返回类型可以明显看出,静态工厂必须记录返回对象的线程安全性,如 Collections.synchronizedMap 演示的那样。
  当类提交使用公共可访问的锁时,它允许客户端原子地执行一系列方法调用,但是这种灵活性是有代价的。它与诸如 ConcurrentHashMap 等并发集合所使用的高性能内部并发控制不兼容。另外,客户机可以通过长时间持有公共可访问锁来发起拒绝服务攻击。这可以是无意的,也可以是有意的。
  为了防止这种拒绝服务攻击,你可以使用一个私有锁对象,而不是使用同步方法(这意味着一个公共可访问的锁):

// Private lock object idiom - thwarts denial-of-service attack
private final Object lock = new Object();
public void foo() { 
    synchronized(lock) {
        ... 
    }
}

  因为私有锁对象在类之外不可访问,所以客户端不可能干扰对象的同步。实际上,我们通过将锁对象封装在它同步的对象中来应用 item 15 的通知。
  注意,锁字段被声明为 final。这样可以防止您无意中更改其内容,从而导致灾难性的非同步访问(item 78)。通过最小化锁字段的可变性,我们应用了 item 17 的建议。锁字段应该总是生命为 final。无论您使用普通的监视器锁(如上所示)还是java.util.concurrent.locks 提供的锁工具。
  私有锁对象习惯用法只能在无条件线程安全的类上使用。有条件线程安全类不能使用这种习惯用法,因为它们必须记录在执行某些方法调用序列时,它们的客户端要获取哪些锁。
  私有锁对象习惯用法特别适合为继承而设计的类(item 19)。如果这样的类要使用它的实例进行锁定,那么子类就可以很容易地、无意地干扰基类的操作,反之亦然。由于将同一个锁用于不同的目的,子类和基类最终可能会“踩到对方的脚”。“这不仅仅是一个理论问题;它发生在 Thread 类[Bloch05, Puzzle 77] 上。
  总而言之,每个类都应该用严谨的文字描述或线程安全注释清楚地记录其线程安全属性。同步修饰符在本文档中不扮演任何角色。有条件线程安全的类必须记录哪些方法调用序列需要外部同步,以及在执行这些序列时需要获取哪些锁。如果您编写了无条件线程安全的类,请考虑使用私有锁对象来代替同步方法。这可以保护您不受客户机和子类的同步干扰,并为您在以后的版本中采用复杂的并发控制方法提供了更大的灵活性。

你可能感兴趣的:(ITEM 82: 使用文档记录线程安全)