0%

Effective Java 第三版 (10) 并发

并发

§ 同步访问共享的可变数据

关键字 synchronized 可以保证同一时刻,只有一个线程可以执行某一个方法,或者某一个代码块。

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

下面代码期望是程序运行大约一秒左右,然后主线程 stopRequested 设置为 true,期望后台线程循环终止。但是这个程序永远不会终止,因为后台线程“看不到”主线程对 stopRequested 的值所做的改变。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
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++;
}
System.out.println(i);
});
backgroundThread.start();

TimeUnit.SECONDS.sleep(1);
stopRequested = true;
}
}

修正这个问题的一种方式是使用同步访问 stopRequested 域,并且必须读写操作都是用同步。

除非读和写操作都被同步,否则无法保证同步能起作用。 有时候,会在某些机器上看到只同步了写(或读)操作的程序看起来也能正常工作,但是在这种情况下,具有很大的欺骗性。

1
2
3
4
5
6
7
8
9
10

private static boolean stopRequested;

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

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

如上所示,上述被同步的方法动作即使没有同步也是原子的,这些方法的同步只是为了它的 通信效果,而不是为了互斥访问。

所以有更好的替代方法:即使用 volatile 关键字。

1
private static volatile boolean stopRequested;

使用 volatile 的时候务必要小心。volatile 只能保证可见性,不能提供互斥 的效果:

1
2
3
4
5
private static volatile int nextSerialNumber = 0;

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

如上所示,++ 操作不是原子性的,这时候应该使用 synchronized 修饰,同时删除 volatile。

§ 避免过度的同步

通常来说,应该在同步区域内做尽可能少的工作。

CopyOnWriteArrayList, 比较适合经常被遍历,又几乎不改动的场景。

§ executor、task、stream 优先于线程

1
2
3
4
5
ExecutorService exec = Executors.newSingleThreadExecutor();

exec.execute(runnable);

exec.shutdown();

或者可以直接使用 ThreadPoolExecutor 类,它允许你控制线程池操作的几乎所有方面。这是阿里巴巴 Java 开发手册上的建议。

§ 并发工具优先于 wait 和 notify

现在几乎没有理由再使用 wait 和 notify 了。

Executor 、并发集合以及同步器。

优先使用 ConcurrentHashMap,而不是 Collections.synchronizedMap 或者 Hashtable

§ 线程安全性的文档化

  • 不可变的。类实例是不可变的,不需要同步。如 StringLongBigInteger
  • 无条件的线程安全。实例是可变的,但是这个类有足够的内部同步,所以它的实例可以并发使用,无需外部同步。如 AtomicLongConcurrentHashMap
  • 有条件的线程安全。除了有些方法为进行安全的并发使用需要外部同步之外,和无条件线程安全相同。如 Collections.synchronized
  • 非线程安全。需要自行外部同步包围。如 ArrayListHashMap

§ 慎用延迟初始化

延迟初始化是一把双刃剑。它降低了初始化类或创建实例的成本,代价是增加了访问延迟初始化字段的成本。

延迟初始化也有它的用途。如果一个字段只在类的一小部分实例上访问,并且初始化该字段的代价很高,那么延迟初始化可能是值得的。

在存在多个线程的情况下,使用延迟初始化很棘手。如果两个或多个线程共享一个延迟初始化的字段,那么必须使用某种形式的同步,否则会导致严重的错误。

§ 不要依赖于线程调度器

当有多个线程可以运行时,线程调度器决定哪些线程将会运行,但是不应该依赖这种调度策略。

线程优先级也是最不可移植的一个特性。