并发
§ 同步访问共享的可变数据
关键字 synchronized
可以保证同一时刻,只有一个线程可以执行某一个方法,或者某一个代码块。
同步的概念 不仅仅是一种互斥 的方式。同步不仅可以阻止一个线程看到对象处于不一致的状态之中,它还可以保证进入同步方法或者同步代码块的每个线程,都看到由同一个锁保护的之前的所有修改效果。
下面代码期望是程序运行大约一秒左右,然后主线程 stopRequested
设置为 true,期望后台线程循环终止。但是这个程序永远不会终止,因为后台线程“看不到”主线程对 stopRequested 的值所做的改变。
1 | public class StopThread { |
修正这个问题的一种方式是使用同步访问 stopRequested 域,并且必须读写操作都是用同步。
除非读和写操作都被同步,否则无法保证同步能起作用。 有时候,会在某些机器上看到只同步了写(或读)操作的程序看起来也能正常工作,但是在这种情况下,具有很大的欺骗性。
1 |
|
如上所示,上述被同步的方法动作即使没有同步也是原子的,这些方法的同步只是为了它的 通信效果,而不是为了互斥访问。
所以有更好的替代方法:即使用 volatile
关键字。
1 | private static volatile boolean stopRequested; |
使用 volatile
的时候务必要小心。volatile
只能保证可见性,不能提供互斥 的效果:
1 | private static volatile int nextSerialNumber = 0; |
如上所示,++ 操作不是原子性的,这时候应该使用 synchronized 修饰,同时删除 volatile。
§ 避免过度的同步
通常来说,应该在同步区域内做尽可能少的工作。
如 CopyOnWriteArrayList
, 比较适合经常被遍历,又几乎不改动的场景。
§ executor、task、stream 优先于线程
1 | ExecutorService exec = Executors.newSingleThreadExecutor(); |
或者可以直接使用 ThreadPoolExecutor
类,它允许你控制线程池操作的几乎所有方面。这是阿里巴巴 Java 开发手册上的建议。
§ 并发工具优先于 wait 和 notify
现在几乎没有理由再使用 wait 和 notify 了。
Executor 、并发集合以及同步器。
优先使用 ConcurrentHashMap
,而不是 Collections.synchronizedMap
或者 Hashtable
§ 线程安全性的文档化
- 不可变的。类实例是不可变的,不需要同步。如
String
、Long
、BigInteger
- 无条件的线程安全。实例是可变的,但是这个类有足够的内部同步,所以它的实例可以并发使用,无需外部同步。如
AtomicLong
和ConcurrentHashMap
- 有条件的线程安全。除了有些方法为进行安全的并发使用需要外部同步之外,和无条件线程安全相同。如
Collections.synchronized
- 非线程安全。需要自行外部同步包围。如
ArrayList
和HashMap
§ 慎用延迟初始化
延迟初始化是一把双刃剑。它降低了初始化类或创建实例的成本,代价是增加了访问延迟初始化字段的成本。
延迟初始化也有它的用途。如果一个字段只在类的一小部分实例上访问,并且初始化该字段的代价很高,那么延迟初始化可能是值得的。
在存在多个线程的情况下,使用延迟初始化很棘手。如果两个或多个线程共享一个延迟初始化的字段,那么必须使用某种形式的同步,否则会导致严重的错误。
§ 不要依赖于线程调度器
当有多个线程可以运行时,线程调度器决定哪些线程将会运行,但是不应该依赖这种调度策略。
线程优先级也是最不可移植的一个特性。