高效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以上的版本。

并发

线程允许多个活动同时进行。 并发编程比单线程编程更难,因为更多的事情可能会出错,并且失败很难重现。 你无法避免并发。 它是平台中固有的,也是要从多核处理器获得良好性能的要求,现在无处不在。本章包含的建议可帮助你编写清晰,正确,文档完备的并发程序。

78. 同步访问共享的可变数据

synchronized关键字确保一次只有一个线程可以执行一个方法或代码块。许多程序员认为同步只是一种互斥的方法,以防止一个线程在另一个线程修改对象时看到对象处于不一致的状态。在这个观点中,对象以一致的状态创建(条目 17),并由访问它的方法锁定。这些方法观察状态,并可选地引起状态转换,将对象从一个一致的状态转换为另一个一致的状态。正确使用同步可以保证没有任何方法会观察到处于不一致状态的对象。

这种观点是正确的,但它只说明了一部分意义。如果没有同步,一个线程的更改可能对其他线程不可见。同步不仅阻止线程观察处于不一致状态的对象,而且确保每个进入同步方法或块的线程都能看到由同一锁保护的所有之前修改的效果。

语言规范保证读取或写入变量是原子性(atomic)的,除非变量的类型是long或double [JLS, 17.4, 17.7]。换句话说,读取long或double以外的变量,可以保证返回某个线程存储到该变量中的值,即使多个线程在没有同步的情况下同时修改变量也是如此。

你可能听说过,为了提高性能,在读取或写入原子数据时应该避免同步。这种建议大错特错。虽然语言规范保证线程在读取属性时不会看到任意值,但它不保证由一个线程编写的值对另一个线程可见。同步是线程之间可靠通信以及互斥所必需的。这是语言规范中称之为内存模型(memory model)的一部分,它规定了一个线程所做的更改何时以及如何对其他线程可见[JLS, 17.4;Goetz06, 16)。

即使数据是原子可读和可写的,未能同步对共享可变数据的访问的后果也是可怕的。 考虑从另一个线程停止一个线程的任务。 Java类库提供了Thread.stop方法,但是这个方法很久以前就被弃用了,因为它本质上是不安全的——它的使用会导致数据损坏。 不要使用Thread.stop。 从另一个线程中停止一个线程的推荐方法是让第一个线程轮询一个最初为false的布尔类型的属性,但是第二个线程可以设置为true以指示第一个线程要自行停止。 因为读取和写入布尔属性是原子的,所以一些程序员在访问属性时不需要同步:

// Broken! - How long would you expect this program to run?
public class StopThread {
    private static boolean stopRequested;

    public static void main(String[] args)
            throws InterruptedException {
        Thread backgroundThread = new Thread(() -> {
            int i = 0;
            while (!stopRequested)
                i++;
        });
        backgroundThread.start();
        TimeUnit.SECONDS.sleep(1);
        stopRequested = true;
    }
}

你可能希望这个程序运行大约一秒钟,之后主线程将stoprequired设置为true,从而导致后台线程的循环终止。然而,在我的机器上,程序永远不会终止:后台线程永远循环!

问题是在没有同步的情况下,无法确保后台线程何时(如果有的话)看到主线程所做的stopRequested值的变化。 在没有同步的情况下,虚拟机将下面代码:

   while (!stopRequested)
        i++;

转换成这样:

if (!stopRequested)
    while (true)
        i++;

这种优化称为提升(hoisting,它正是OpenJDK Server VM所做的。 结果是活泼失败( liveness failure):程序无法取得进展。 解决问题的一种方法是同步对stopRequested属性的访问。 正如预期的那样,该程序大约一秒钟终止:

// Properly synchronized cooperative thread termination
public class StopThread {
    private static boolean stopRequested;

    private static synchronized void requestStop() {
        stopRequested = true;
    }

    private static synchronized boolean stopRequested() {
        return stopRequested;
    }

    public static void main(String[] args)
            throws InterruptedException {
        Thread backgroundThread = new Thread(() -> {
            int i = 0;
            while (!stopRequested())
                i++;
        });

        backgroundThread.start();
        TimeUnit.SECONDS.sleep(1);
        requestStop();
    }
}

注意,写方法(requestStop)和读方法(stop- required)都是同步的。仅同步写方法是不够的!除非读和写操作同步,否则不能保证同步工作。有时,只同步写(或读)的程序可能在某些机器上显示有效,但在这种情况下,表面的现象是具有欺骗性的。

即使没有同步,StopThread中同步方法的操作也是原子性的。换句话说,这些方法上的同步仅用于其通信效果,而不是互斥。虽然在循环的每个迭代上同步的成本很小,但是有一种正确的替代方法,它不那么冗长,而且性能可能更好。如果stoprequest声明为volatile,则可以省略StopThread的第二个版本中的锁定。虽然volatile修饰符不执行互斥,但它保证任何读取属性的线程都会看到最近写入的值:

// Cooperative thread termination with a volatile field
public class StopThread {
    private static volatile boolean stopRequested;

    public static void main(String[] args)
            throws InterruptedException {
        Thread backgroundThread = new Thread(() -> {
            int i = 0;
            while (!stopRequested)
                i++;
        });
        backgroundThread.start();
        TimeUnit.SECONDS.sleep(1);
        stopRequested = true;
    }
}

在使用volatile时一定要小心。考虑下面的方法,该方法应该生成序列号:

// Broken - requires synchronization!
private static volatile int nextSerialNumber = 0;

public static int generateSerialNumber() {
    return nextSerialNumber++;
}

该方法的目的是保证每次调用都返回一个唯一值(只要调用次数不超过232次)。 方法的状态由单个可原子访问的属性nextSerialNumber组成,该属性的所有可能值都是合法的。 因此,不需要同步来保护其不变量。 但是,如果没有同步,该方法将无法正常工作。

问题是增量运算符(++)不是原子的。 它对nextSerialNumber属性执行两个操作:首先它读取值,然后它写回一个新值,等于旧值加1。 如果第二个线程在线程读取旧值并写回新值之间读取属性,则第二个线程将看到与第一个线程相同的值并返回相同的序列号。 这是安全性失败(safety failure):程序计算错误的结果。

修复generateSerialNumber的一种方法是将synchronized修饰符添加到其声明中。 这确保了多个调用不会交叉读取,并且每次调用该方法都会看到所有先前调用的效果。 完成后,可以并且应该从nextSerialNumber中删除volatile修饰符。 要保护该方法,请使用long而不是int,或者在nextSerialNumber即将包装时抛出异常。

更好的是,遵循条目 59条中建议并使用AtomicLong类,它是java.util.concurrent.atomic包下的一部分。 这个包为单个变量提供了无锁,线程安全编程的基本类型。 虽然volatile只提供同步的通信效果,但这个包还提供了原子性。 这正是我们想要的generateSerialNumber,它可能强于同步版本的代码:

// Lock-free synchronization with java.util.concurrent.atomic
private static final AtomicLong nextSerialNum = new AtomicLong();

public static long generateSerialNumber() {
    return nextSerialNum.getAndIncrement();
}

避免此条目中讨论的问题的最佳方法是不共享可变数据。 共享不可变数据(条目 17)或根本不共享。 换句话说,将可变数据限制在单个线程中。 如果采用此策略,则必须对其进行文档记录,以便在程序发展改进时维护此策略。 深入了解正在使用的框架和类库也很重要,因为它们可能会引入你不知道的线程。

一个线程可以修改一个数据对象一段时间后,然后与其他线程共享它,只同步共享对象引用的操作。然后,其他线程可以在不进一步同步的情况下读取对象,只要不再次修改该对象。这些对象被认为是有效不可变的( effectively immutable)[Goetz06, 3.5.4]。将这样的对象引用从一个线程转移到其他线程称为安全发布(safe publication )[Goetz06, 3.5.3]。安全地发布对象引用的方法有很多:可以将它保存在静态属性中,作为类初始化的一部分;也可以将其保存在volatile属性、final属性或使用正常锁定访问的属性中;或者可以将其放入并发集合中(条目 81)。

总之,当多个线程共享可变数据时,每个读取或写入数据的线程都必须执行同步。 在没有同步的情况下,无法保证一个线程的更改对另一个线程可见。 未能同步共享可变数据的代价是活性失败和安全性失败。 这些失败是最难调试的。 它们可以是间歇性的和时间相关的,并且程序行为可能在不同VM之间发生根本的变化。如果只需要线程间通信,而不需要互斥,那么volatile修饰符是一种可接受的同步形式,但是正确使用它可能会比较棘手。