高效Java(第三版) Effective Java
1. 考虑使用静态工厂方法替代构造方法 2. 当构造方法参数过多时使用builder模式 3. 使用私有构造方法或枚类实现Singleton属性 4. 使用私有构造方法执行非实例化 5. 使用依赖注入取代硬连接资源 6. 避免创建不必要的对象 7. 消除过期的对象引用 8. 避免使用Finalizer和Cleaner机制 9. 使用try-with-resources语句替代try-finally语句 10. 重写equals方法时遵守通用约定 11. 重写equals方法时同时也要重写hashcode方法 12. 始终重写 toString 方法 13. 谨慎地重写 clone 方法 14. 考虑实现Comparable接口 15. 使类和成员的可访问性最小化 16. 在公共类中使用访问方法而不是公共属性 17. 最小化可变性 18. 组合优于继承 19. 如果使用继承则设计,并文档说明,否则不该使用 20. 接口优于抽象类 21. 为后代设计接口 22. 接口仅用来定义类型 23. 优先使用类层次而不是标签类 24. 优先考虑静态成员类 25. 将源文件限制为单个顶级类 26. 不要使用原始类型 27. 消除非检查警告 28. 列表优于数组 29. 优先考虑泛型 30. 优先使用泛型方法 31. 使用限定通配符来增加API的灵活性 32. 合理地结合泛型和可变参数 33. 优先考虑类型安全的异构容器 34. 使用枚举类型替代整型常量 35. 使用实例属性替代序数 36. 使用EnumSet替代位属性 37. 使用EnumMap替代序数索引 38. 使用接口模拟可扩展的枚举 39. 注解优于命名模式 40. 始终使用Override注解 41. 使用标记接口定义类型 42. lambda表达式优于匿名类 43. 方法引用优于lambda表达式 44. 优先使用标准的函数式接口 45. 明智审慎地使用Stream 46. 优先考虑流中无副作用的函数 47. 优先使用Collection而不是Stream来作为方法的返回类型 48. 谨慎使用流并行 49. 检查参数有效性 50. 必要时进行防御性拷贝 51. 仔细设计方法签名 52. 明智而审慎地使用重载 53. 明智而审慎地使用可变参数 54. 返回空的数组或集合不要返回null 55. 明智而审慎地返回Optional 56. 为所有已公开的API元素编写文档注释 57. 最小化局部变量的作用域 58. for-each循环优于传统for循环 59. 熟悉并使用Java类库 60. 需要精确的结果时避免使用float和double类型 61. 基本类型优于装箱的基本类型 62. 当有其他更合适的类型时就不用字符串 63. 注意字符串连接的性能 64. 通过对象的接口引用对象 65. 接口优于反射 66. 明智谨慎地使用本地方法 67. 明智谨慎地进行优化 68. 遵守普遍接受的命名约定 69. 仅在发生异常的条件下使用异常 70. 对可恢复条件使用检查异常,对编程错误使用运行时异常 71. 避免不必要地使用检查异常 72. 赞成使用标准异常 73. 抛出合乎于抽象的异常 74. 文档化每个方法抛出的所有异常 75. 在详细信息中包含失败捕获信息 76. 争取保持失败原子性 77. 同步访问共享的可变数据 78. 避免过度同步 79. EXECUTORS, TASKS, STREAMS 优于线程 80. 优先使用并发实用程序替代wait和notify 81. 线程安全文档化 82. 明智谨慎地使用延迟初始化 83. 不要依赖线程调度器 84. 其他替代方式优于Java本身序列化 85. 非常谨慎地实现SERIALIZABLE接口 86. 考虑使用自定义序列化形式 87. 防御性地编写READOBJECT方法 88. 对于实例控制,枚举类型优于READRESOLVE 89. 考虑序列化代理替代序列化实例

线程安全文档化

Tips
书中的源代码地址:https://github.com/jbloch/effective-java-3e-source-code
注意,书中的有些代码里方法是基于Java 9 API中的,所以JDK 最好下载 JDK 9以上的版本。

82. 线程安全文档化

当并发使用一个类的方法时,类的行为方式是其与客户端建立约定的重要部分。如果未能文档化记录某个类行为的这一方面,其用户只能做出做出假设。如果这些假设是错误的,则生成的程序可能执行同步不够(条目 78)或过度同步(条目 79)。 无论哪种情况,都可能导致严重错误。

你可能听说过,可以通过在方法的文档中查找synchronized修饰符来判断该方法是否线程安全的。这在几个方面来讲是错误的。在正常操作中,Javadoc的输出中没有包含synchronized修饰符,这是有原因的。方法声明中synchronized修饰符的存在是实现细节,而不是其API的一部分。它不能可靠地说明方法是是线程安全的。

此外,声称存在synchronized修饰符就足以文档记录线程安全性,这体现了线程安全性是要么全有要么全无属性的误解。实际上,线程安全有几个级别。要启用安全的并发使用,类必须清楚地文档记录它支持的线程安全级别。下面的列表总结了线程安全级别。它并非详尽无遗,但涵盖以下常见情况:

  • 不可变的(Immutable) —— 该类的实例看起来是不变的(constant)。不需要外部同步。示例包括String、Long和BigInteger(条目 17)。

  • 无条件线程安全(Unconditionally thread-safe) —— 此类的实例是可变的,但该类具有足够的内部同步,以便可以并发使用其实例而无需任何外部同步。 示例包括AtomicLong和ConcurrentHashMap。

  • 有条件线程安全(Conditionally thread-safe) —— 与无条件线程安全一样,但某些方法需要外部同步以便安全并发使用。 示例包括Collections.synchronized包装器返回的集合,其迭代器需要外部同步。

  • 非线程安全(Not thread-safe) —— 这个类的实例是可变的。 要并发使用它们,客户端必须使用其选择的外部同步来包围每个方法调用(或调用序列)。 示例包括通用集合实现,例如ArrayList和HashMap。

  • 线程对立(Thread-hostile) —— 即使每个方法调用都被外部同步包围,该类对于并发使用也是不安全的。线程对立通常是由于在不同步的情况下修改静态数据而导致的。没有人故意编写线程对立类;此类通常是由于没有考虑并发性而导致的。当发现类或方法与线程不相容时,通常将其修正或弃用。条目 78中的generateSerialNumber方法在没有内部同步的情况下是线程对立的,如第322页所述。

这些分类(除了线程对立)大致对应于《Java Concurrency in Practice》一书中的线程安全注解,分别是Immutable,ThreadSafe和NotThreadSafe [Goetz06,附录A]。 上述分类中的无条件和条件线程安全类别都包含在ThreadSafe注解中。

在文档记录了一个有条件的线程安全类需要小心。 你必须指明哪些调用序列需要外部同步,以及必须获取哪个锁(或在极少数情况下是几把锁)才能执行这些序列。 通常是实例本身的锁,但也有例外。 例如,Collections.synchronizedMap的文档说明了这一点:

It is imperative that the user manually synchronize on the returned map when iterating over any of its collection views:
当迭代任何Map集合的视图时,用户必须手动同步返回的Map:

Map<K, V> m = Collections.synchronizedMap(new HashMap<>());
Set<K> 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)使用的高性能内部并发控制不兼容。 此外,客户端可以通过长时间保持可公开访问的锁来发起拒绝服务攻击。 这可能是偶然也可能是故意的。

要防止此拒绝服务攻击,可以使用私有锁对象而不是使用synchronized方法(这隐含着可公开访问的锁):

// Private lock object idiom - thwarts denial-of-service attack
private final Object lock = new Object();

public void foo() {
    synchronized(lock) {
        ...
    }
}

由于私有锁对象在类外是不可访问的,因此客户端不可能干扰对象的同步。 实际上,我们通过将锁定对象封装在它同步的对象中来应用条目 15的建议。

请注意,锁定属性(lock field)被声明为final。 这可以防止无意中更改其内容,从而导致灾难性的非同步访问(条目 78)。 我们通过最小化锁定属性(lock field)的可变性来应用条目 17的建议。 锁定属性(lock field)应始终声明为final。 无论使用普通的监视器锁(如上所示)还是使用java.util.concurrent.locks包中的锁,都是如此。

私有锁对象习惯用法只能用于无条件线程安全类。 有条件线程安全类不能使用这个习惯用法,因为它们必须文档记录在执行某些方法调用序列时客户端要获取的锁。

私有锁对象习惯用法特别适合用于为继承设计的类(条目 19)。 如果这样的类要使用其实例进行锁定,则子类可能容易且无意地干扰基类的操作,反之亦然。 通过为不同的目的使用相同的锁,子类和基类可能最终“踩到彼此的脚趾。”这不仅仅是一个理论问题;它就发生在Thread类上[Bloch05,Puzzle 77]中。

总之,每个类都应该用措辞严谨的描述或线程安全注解清楚地文档记录其线程安全属性。synchronized修饰符在本文档中没有任何作用。条件线程安全类必须文档记录哪些方法调用序列需要外部同步,以及在执行这些序列时需要获取哪些锁。如果你编写一个无条件线程安全的类,请考虑使用一个私有锁对象来代替同步方法。这将保护免受客户端和子类的同步干扰,并提供更大的灵活性,以便在后续的版本中采用复杂的并发控制方法。