同步不仅可以阻止一个线程看到对象处于不一致的状态中,它还可以保证进入同步方法或者同步代码块的每个线程,都能看到由同一个锁保护的之前所有的修改效果。
为了在线程之间进行可靠的通信,也为了互斥访问,同步是必要的。除非读和写操作都被同步,否则无法保证同步能起作用。
千万不要使用Thread.stop方法,因为它是不安全的,会导致数据遭到破坏。
要么共享不可变的数据,要么不共享。将可变数据限制在单个线程中。
未能同步共享的可变数据会造成程序的活性失败(liveness failure)和安全性失败(safety failure)。活性失败指的是虚拟机对代码优化提升(hoisting)而导致的失败。安全性失败指对象状态不一致导致的安全性问题。谨慎地使用volatile。
为了避免活性失败和安全性失败,在一个被同步的方法或代码块中,永远不要放弃对客户端的控制。在一个被同步的区域内部,不要调用设计成要被覆盖的方法,或者是由客户端以函数对象的形式提供的方法。
应该在同步区域内做尽可能少的工作。获得锁,检查共享数据,根据需要转换数据,然后释放锁。如果要执行某个耗时的动作,应该设法将这个动作移到同步区域外面。为了避免死锁和数据破坏,千万不要从同步区域内部调用外来方法。
过度同步的实际成本并不是指获取锁锁花费的CPU时间;而是指失去了并行的机会,以及因为需要确保每个核都有一个一致的内存视图而导致的延迟;另一项潜在开销在于,它会限制虚拟机优化代码执行的能力。
轻量负载的服务器,建议使用Executors.newCachedThreadPool(被提交的任务没有排成队列,而是直接交给线程执行)。但对大负载的服务器,最好使用Executors.newFixedThreadPool,它提供固定线程数目的线程池。
不仅不应该编写自己的工作队列,而且还应该不直接使用线程。任务task有两种:Runnable和Callable(有返回值,能抛出异常)。
fork-join任务是通过一种称作fork-join池的特殊executor服务允许的。CPU使用率、吞吐量更高,延迟更小。并发的stream是在fork-join池上编写的。
正确地使用wait和notify比较困难,应该用更高级的并发工具java.util.concurrent代替。
没有理由在新代码中使用wait和notify方法,即使有,也极少。如果用,应该始终使用while循环模式内部来调用wait方法,永远不要在循环之外调用wait。一般情况下,应优先使用notifyAll方法,而不是notify方法,使用后者要小心确保程序的活性。
并发集合中不可能排除并发活动,将它锁定没有什么用,只会使程序变慢。应该优先使用ConcurrentHashMap,而不是Collections.synchronizedMap。只要用并发Map替换同步Map就可以提升并发应用程序的性能。
对于间歇式的定时,始终应该优先使用System.nanoTime,而不是使用System.currentTimeMillis,因为前者更精确。
82.1 一个类为了可被多个线程安全地使用,必须在文档中清楚地说明它所支持的线程安全性级别。
a. 不可变的(immutable)。类的实例是不可变的,所以不需要外部同步,如String、Long、Big Integer。
b. 无条件的线程安全。类的实例是可变的,但有足够的内部同步,所以不需要外部同步,如AtomicLong和ConcurrentHashMap。
c. 有条件的线程安全。个别方法需要外部同步,如Collections.synchronized包装返回的集合,它们的迭代器要求外部同步。
d. 非线程安全。需要外部同步,如ArrayList和HashMap。
e. 线程对立的。根源在于没有同步地修改静态数据。
82.2 私有锁对象模式只能用在无条件的线程安全类上,特别适用于那些专门为继承而设计的类。这样可以防止客户端程序和子类的不同步干扰。lock域应该始终声明为final。
82.3 公有锁可能导致拒绝服务攻击,客户端只需超时地保持公有可访问锁。
延迟初始化是指延迟到需要域的值时才将它初始化的行为。在大多数情况下,正常的初始化要优先于延迟初始化。
出于性能考虑而需延迟初始化的场景:
a. 对静态域,使用lazy initialization holder class模式,即用一个静态私有类Holder来持有这个静态域,外部类通过这个类来访问静态域。
b. 对实例域,使用双重检查模式。第一次检查不锁定,看看这个域是否被初始化。第二次检查时锁定,如果这个域没被初始化,才会进行初始化。注意这个域需声明为volatile。
c. 对可接受重复初始化的实例域,考虑使用单重检查模式。
不要让应用程序的正确性依赖于线程调度器。否则,这个应用程序将既不健壮,也不具有可移植性。要编写出健壮、响应良好、可移植的多线程应用程序,最好的办法是确保可运行线程的平均数量不明显多于处理器的数量。保持可运行线程数量尽可能少的方法是,让每个线程做些有意义的工作,然后等待更多有意义的工作。
不要企图通过调用Thread.yield来“修正”程序,因为这样的程序不可移植,同一个yield调用在不同的JVM实现上是不一样的。
通过调整某些线程的优先级来改善应用程序的响应性能,是不必要的,也是不可移植的。