《Effective Java》知识点(10)--并发

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

        同步不仅可以阻止一个线程看到对象处于不一致的状态中,它还可以保证进入同步方法或者同步代码块的每个线程,都能看到由同一个锁保护的之前所有的修改效果。

         为了在线程之间进行可靠的通信,也为了互斥访问,同步是必要的。除非读和写操作都被同步,否则无法保证同步能起作用。

         千万不要使用Thread.stop方法,因为它是不安全的,会导致数据遭到破坏。

         要么共享不可变的数据,要么不共享。将可变数据限制在单个线程中。

         未能同步共享的可变数据会造成程序的活性失败(liveness failure)和安全性失败(safety failure)。活性失败指的是虚拟机对代码优化提升(hoisting)而导致的失败。安全性失败指对象状态不一致导致的安全性问题。谨慎地使用volatile。

79. 避免过度同步

       为了避免活性失败和安全性失败,在一个被同步的方法或代码块中,永远不要放弃对客户端的控制。在一个被同步的区域内部,不要调用设计成要被覆盖的方法,或者是由客户端以函数对象的形式提供的方法。

       应该在同步区域内做尽可能少的工作。获得锁,检查共享数据,根据需要转换数据,然后释放锁。如果要执行某个耗时的动作,应该设法将这个动作移到同步区域外面。为了避免死锁和数据破坏,千万不要从同步区域内部调用外来方法。  

       过度同步的实际成本并不是指获取锁锁花费的CPU时间;而是指失去了并行的机会,以及因为需要确保每个核都有一个一致的内存视图而导致的延迟;另一项潜在开销在于,它会限制虚拟机优化代码执行的能力。

80. executor、task和stream优先于线程

       轻量负载的服务器,建议使用Executors.newCachedThreadPool(被提交的任务没有排成队列,而是直接交给线程执行)。但对大负载的服务器,最好使用Executors.newFixedThreadPool,它提供固定线程数目的线程池。

        不仅不应该编写自己的工作队列,而且还应该不直接使用线程。任务task有两种:Runnable和Callable(有返回值,能抛出异常)。

        fork-join任务是通过一种称作fork-join池的特殊executor服务允许的。CPU使用率、吞吐量更高,延迟更小。并发的stream是在fork-join池上编写的。

81. 并发工具优先于wait和notify

       正确地使用wait和notify比较困难,应该用更高级的并发工具java.util.concurrent代替。

       没有理由在新代码中使用wait和notify方法,即使有,也极少。如果用,应该始终使用while循环模式内部来调用wait方法,永远不要在循环之外调用wait。一般情况下,应优先使用notifyAll方法,而不是notify方法,使用后者要小心确保程序的活性。

       并发集合中不可能排除并发活动,将它锁定没有什么用,只会使程序变慢。应该优先使用ConcurrentHashMap,而不是Collections.synchronizedMap。只要用并发Map替换同步Map就可以提升并发应用程序的性能。

        对于间歇式的定时,始终应该优先使用System.nanoTime,而不是使用System.currentTimeMillis,因为前者更精确。

82. 线程安全性的文档化

82.1 一个类为了可被多个线程安全地使用,必须在文档中清楚地说明它所支持的线程安全性级别。

       a. 不可变的(immutable)。类的实例是不可变的,所以不需要外部同步,如String、Long、Big Integer。

       b. 无条件的线程安全。类的实例是可变的,但有足够的内部同步,所以不需要外部同步,如AtomicLong和ConcurrentHashMap。

       c. 有条件的线程安全。个别方法需要外部同步,如Collections.synchronized包装返回的集合,它们的迭代器要求外部同步。

       d. 非线程安全。需要外部同步,如ArrayList和HashMap。

       e. 线程对立的。根源在于没有同步地修改静态数据。

82.2 私有锁对象模式只能用在无条件的线程安全类上,特别适用于那些专门为继承而设计的类。这样可以防止客户端程序和子类的不同步干扰。lock域应该始终声明为final。

82.3 公有锁可能导致拒绝服务攻击,客户端只需超时地保持公有可访问锁。

83. 慎用延迟初始化

      延迟初始化是指延迟到需要域的值时才将它初始化的行为。在大多数情况下,正常的初始化要优先于延迟初始化。

     出于性能考虑而需延迟初始化的场景:

  a. 对静态域,使用lazy initialization holder class模式,即用一个静态私有类Holder来持有这个静态域,外部类通过这个类来访问静态域。

  b. 对实例域,使用双重检查模式。第一次检查不锁定,看看这个域是否被初始化。第二次检查时锁定,如果这个域没被初始化,才会进行初始化。注意这个域需声明为volatile。

  c. 对可接受重复初始化的实例域,考虑使用单重检查模式。

84. 不要依赖于线程调度器

       不要让应用程序的正确性依赖于线程调度器。否则,这个应用程序将既不健壮,也不具有可移植性。要编写出健壮、响应良好、可移植的多线程应用程序,最好的办法是确保可运行线程的平均数量不明显多于处理器的数量。保持可运行线程数量尽可能少的方法是,让每个线程做些有意义的工作,然后等待更多有意义的工作。

       不要企图通过调用Thread.yield来“修正”程序,因为这样的程序不可移植,同一个yield调用在不同的JVM实现上是不一样的。

       通过调整某些线程的优先级来改善应用程序的响应性能,是不必要的,也是不可移植的。

        

         

你可能感兴趣的:(Java,java)