第 11 章 并发

【78】对共享可变数据的同步访问

Synchronize access to shared mutable data

The synchronized keyword ensures that only a single thread can execute a method or block at one time. Many programmers think of synchronization solely as a means of mutual exclusion, to prevent an object from being seen in an inconsistent state by one thread while it’s being modified by another. In this view, an object is created in a consistent state (Item 17) and locked by the methods that access it. These methods observe the state and optionally cause a state transition, transforming the object from one consistent state to another. Proper use of synchronization guarantees that no method will ever observe the object in an inconsistent state.

synchronized 关键字确保一次只有一个线程可以执行一个方法或块。许多程序员认为同步只是一种互斥的方法,是为防止一个线程在另一个线程修改对象时使对象处于不一致的状态。这样看来,对象以一致的状态创建(Item-17),并由访问它的方法锁定。这些方法可以察觉当前状态,并引起状态转换,将对象从一致的状态转换为另一个一致的状态。正确使用同步可以保证没有方法会让对象处于不一致状态。

This view is correct, but it’s only half the story. Without synchronization, one thread’s changes might not be visible to other threads. Not only does synchronization prevent threads from observing an object in an inconsistent state, but it ensures that each thread entering a synchronized method or block sees the effects of all previous modifications that were guarded by the same lock.

这种观点是正确的,但它只是冰山一角。没有同步,一个线程所做的的更改可能对其他线程不可见。同步不仅阻止线程察觉到处于不一致状态的对象,而且确保每个进入同步方法或块的线程都能察觉由同一把锁保护的所有已修改的效果。

The language specification guarantees that reading or writing a variable is atomic unless the variable is of type long or double [JLS, 17.4, 17.7]. In other words, reading a variable other than a long or double is guaranteed to return a value that was stored into that variable by some thread, even if multiple threads modify the variable concurrently and without synchronization.

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

You may hear it said that to improve performance, you should dispense with synchronization when reading or writing atomic data. This advice is dangerously wrong. While the language specification guarantees that a thread will not see an arbitrary value when reading a field, it does not guarantee that a value written by one thread will be visible to another. Synchronization is required for reliable communication between threads as well as for mutual exclusion. This is due to a part of the language specification known as the memory model, which specifies when and how changes made by one thread become visible to others [JLS, 17.4; Goetz06, 16].

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

The consequences of failing to synchronize access to shared mutable data can be dire even if the data is atomically readable and writable. Consider the task of stopping one thread from another. The libraries provide the Thread.stop method, but this method was deprecated long ago because it is inherently unsafe —its use can result in data corruption. Do not use Thread.stop. A recommended way to stop one thread from another is to have the first thread poll a boolean field that is initially false but can be set to true by the second thread to indicate that the first thread is to stop itself. Because reading and writing a boolean field is atomic, some programmers dispense with synchronization when accessing the field:

即使数据是原子可读和可写的,无法同步访问共享可变数据的后果也可能是可怕的。考虑从一个线程中使另一个线程停止的任务。库提供了 Thread.stop 方法,但是这个方法很久以前就被弃用了,因为它本质上是不安全的,它的使用可能导致数据损坏。不要使用 Thread.stop 一个建议的方法是让第一个线程轮询一个 boolean 字段,该字段最初为 false,但第二个线程可以将其设置为 true,以指示第一个线程要停止它自己。由于读写布尔字段是原子性的,一些程序员在访问该字段时不需要同步:

  1. // Broken! - How long would you expect this program to run?
  2. public class StopThread {
  3. private static boolean stopRequested;
  4. public static void main(String[] args) throws InterruptedException {
  5. Thread backgroundThread = new Thread(() -> {
  6. int i = 0;
  7. while (!stopRequested)
  8. i++;
  9. });
  10. backgroundThread.start();
  11. TimeUnit.SECONDS.sleep(1);
  12. stopRequested = true;
  13. }
  14. }

You might expect this program to run for about a second, after which the main thread sets stopRequested to true, causing the background thread’s loop to terminate. On my machine, however, the program never terminates: the background thread loops forever!

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

The problem is that in the absence of synchronization, there is no guarantee as to when, if ever, the background thread will see the change in the value of stopRequested made by the main thread. In the absence of synchronization, it’s quite acceptable for the virtual machine to transform this code:

问题在于在缺乏同步的情况下,无法保证后台线程何时(如果有的话)看到主线程所做的 stopRequested 值的更改。在缺乏同步的情况下,虚拟机可以很好地转换这段代码:

  1. while (!stopRequested)
  2. i++;
  3. into this code:
  4. if (!stopRequested)
  5. while (true)
  6. i++;

This optimization is known as hoisting, and it is precisely what the OpenJDK Server VM does. The result is a liveness failure: the program fails to make progress. One way to fix the problem is to synchronize access to the stopRequested field. This program terminates in about one second, as expected:

这种优化称为提升,这正是 OpenJDK 服务器 VM 所做的。结果是活性失败:程序无法取得进展。解决此问题的一种方法是同步对 stopRequested 字段的访问。程序在大约一秒内结束,正如预期:

  1. // Properly synchronized cooperative thread termination
  2. public class StopThread {
  3. private static boolean stopRequested;
  4. private static synchronized void requestStop() {
  5. stopRequested = true;
  6. }
  7. private static synchronized boolean stopRequested() {
  8. return stopRequested;
  9. }
  10. public static void main(String[] args) throws InterruptedException {
  11. Thread backgroundThread = new Thread(() -> {
  12. int i = 0;
  13. while (!stopRequested())
  14. i++;
  15. });
  16. backgroundThread.start();
  17. TimeUnit.SECONDS.sleep(1);
  18. requestStop();
  19. }
  20. }

Note that both the write method (requestStop) and the read method (stop-Requested) are synchronized. It is not sufficient to synchronize only the write method! Synchronization is not guaranteed to work unless both read and write operations are synchronized. Occasionally a program that synchronizes only writes (or reads) may appear to work on some machines, but in this case, appearances are deceiving.

注意,写方法(requestStop)和读方法(stopRequested)都是同步的。仅同步写方法是不够的!除非读和写操作都同步,否则不能保证同步工作。 有时,只同步写(或读)的程序可能在某些机器上显示有效,但在这种情况下,不能这么做。

The actions of the synchronized methods in StopThread would be atomic even without synchronization. In other words, the synchronization on these methods is used solely for its communication effects, not for mutual exclusion. While the cost of synchronizing on each iteration of the loop is small, there is a correct alternative that is less verbose and whose performance is likely to be better. The locking in the second version of StopThread can be omitted if stopRequested is declared volatile. While the volatile modifier performs no mutual exclusion, it guarantees that any thread that reads the field will see the most recently written value:

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

  1. // Cooperative thread termination with a volatile field
  2. public class StopThread {
  3. private static volatile boolean stopRequested;
  4. public static void main(String[] args) throws InterruptedException {
  5. Thread backgroundThread = new Thread(() -> {
  6. int i = 0;
  7. while (!stopRequested)
  8. i++;
  9. });
  10. backgroundThread.start();
  11. TimeUnit.SECONDS.sleep(1);
  12. stopRequested = true;
  13. }
  14. }

You do have to be careful when using volatile. Consider the following method, which is supposed to generate serial numbers:

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

  1. // Broken - requires synchronization!
  2. private static volatile int nextSerialNumber = 0;
  3. public static int generateSerialNumber() {
  4. return nextSerialNumber++;
  5. }

The intent of the method is to guarantee that every invocation returns a unique value (so long as there are no more than 232 invocations). The method’s state consists of a single atomically accessible field, nextSerialNumber, and all possible values of this field are legal. Therefore, no synchronization is necessary to protect its invariants. Still, the method won’t work properly without synchronization.

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

The problem is that the increment operator (++) is not atomic. It performs two operations on the nextSerialNumber field: first it reads the value, and then it writes back a new value, equal to the old value plus one. If a second thread reads the field between the time a thread reads the old value and writes back a new one, the second thread will see the same value as the first and return the same serial number. This is a safety failure: the program computes the wrong results.

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

One way to fix generateSerialNumber is to add the synchronized modifier to its declaration. This ensures that multiple invocations won’t be interleaved and that each invocation of the method will see the effects of all previous invocations. Once you’ve done that, you can and should remove the volatile modifier from nextSerialNumber. To bulletproof the method, use long instead of int, or throw an exception if nextSerialNumber is about to wrap.

修复 generateSerialNumber 的一种方法是将 synchronized 修饰符添加到它的声明中。这确保了多个调用不会交叉,并且该方法的每次调用都将看到之前所有调用的效果。一旦你这样做了,你就可以并且应该从 nextSerialNumber 中删除 volatile 修饰符。为了使方法更可靠,应使用 long 而不是 int,或者在 nextSerialNumber 即将超限时抛出异常。

Better still, follow the advice in Item 59 and use the class AtomicLong, which is part of java.util.concurrent.atomic. This package provides primitives for lock-free, thread-safe programming on single variables. While volatile provides only the communication effects of synchronization, this package also provides atomicity. This is exactly what we want for generateSerialNumber, and it is likely to outperform the synchronized version:

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

  1. // Lock-free synchronization with java.util.concurrent.atomic
  2. private static final AtomicLong nextSerialNum = new AtomicLong();
  3. public static long generateSerialNumber() {
  4. return nextSerialNum.getAndIncrement();
  5. }

The best way to avoid the problems discussed in this item is not to share mutable data. Either share immutable data (Item 17) or don’t share at all. In other words, confine mutable data to a single thread. If you adopt this policy, it is important to document it so that the policy is maintained as your program evolves. It is also important to have a deep understanding of the frameworks and libraries you’re using because they may introduce threads that you are unaware of.

为避免出现本条目中讨论的问题,最佳方法是不共享可变数据。要么共享不可变数据(Item-17),要么完全不共享。换句话说,应当将可变数据限制在一个线程中。 如果采用此策略,重要的是对其进行文档化,以便随着程序的发展维护该策略。深入了解你正在使用的框架和库也很重要,因为它们可能会引入你不知道的线程。

It is acceptable for one thread to modify a data object for a while and then to share it with other threads, synchronizing only the act of sharing the object reference. Other threads can then read the object without further synchronization, so long as it isn’t modified again. Such objects are said to be effectively immutable [Goetz06, 3.5.4]. Transferring such an object reference from one thread to others is called safe publication [Goetz06, 3.5.3]. There are many ways to safely publish an object reference: you can store it in a static field as part of class initialization; you can store it in a volatile field, a final field, or a field that is accessed with normal locking; or you can put it into a concurrent collection (Item 81).

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

In summary, when multiple threads share mutable data, each thread that reads or writes the data must perform synchronization. In the absence of synchronization, there is no guarantee that one thread’s changes will be visible to another thread. The penalties for failing to synchronize shared mutable data are liveness and safety failures. These failures are among the most difficult to debug. They can be intermittent and timing-dependent, and program behavior can vary radically from one VM to another. If you need only inter-thread communication, and not mutual exclusion, the volatile modifier is an acceptable form of synchronization, but it can be tricky to use correctly.

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


【79】避免过度同步

Avoid excessive synchronization

Item 78 warns of the dangers of insufficient synchronization. This item concerns the opposite problem. Depending on the situation, excessive synchronization can cause reduced performance, deadlock, or even nondeterministic behavior.

Item-78 警告我们同步不到位的危险。本条目涉及相反的问题。根据不同的情况,过度的同步可能导致性能下降、死锁甚至不确定行为。

To avoid liveness and safety failures, never cede control to the client within a synchronized method or block. In other words, inside a synchronized region, do not invoke a method that is designed to be overridden, or one provided by a client in the form of a function object (Item 24). From the perspective of the class with the synchronized region, such methods are alien. The class has no knowledge of what the method does and has no control over it. Depending on what an alien method does, calling it from a synchronized region can cause exceptions, deadlocks, or data corruption.

为避免活性失败和安全故障,永远不要在同步方法或块中将控制权交给客户端。 换句话说,在同步区域内,不要调用一个设计为被覆盖的方法,或者一个由客户端以函数对象的形式提供的方法(Item-24)。从具有同步区域的类的角度来看,这种方法是不一样的。类不知道该方法做什么,也无法控制它。Depending on what an alien method does,从同步区域调用它可能会导致异常、死锁或数据损坏。

To make this concrete, consider the following class, which implements an observable set wrapper. It allows clients to subscribe to notifications when elements are added to the set. This is the Observer pattern [Gamma95]. For brevity’s sake, the class does not provide notifications when elements are removed from the set, but it would be a simple matter to provide them. This class is implemented atop the reusable ForwardingSet from Item 18 (page 90):

要使这个问题具体化,请考虑下面的类,它实现了一个可视 Set 包装器。当元素被添加到集合中时,它允许客户端订阅通知。这是观察者模式 [Gamma95]。为了简单起见,当元素从集合中删除时,该类不提供通知,即使要提供通知也很简单。这个类是在 Item-18(第 90 页)的可复用 ForwardingSet 上实现的:

  1. // Broken - invokes alien method from synchronized block!
  2. public class ObservableSet<E> extends ForwardingSet<E> {
  3. public ObservableSet(Set<E> set) { super(set); }
  4. private final List<SetObserver<E>> observers= new ArrayList<>();
  5. public void addObserver(SetObserver<E> observer) {
  6. synchronized(observers) {
  7. observers.add(observer);
  8. }
  9. }
  10. public boolean removeObserver(SetObserver<E> observer) {
  11. synchronized(observers) {
  12. return observers.remove(observer);
  13. }
  14. }
  15. private void notifyElementAdded(E element) {
  16. synchronized(observers) {
  17. for (SetObserver<E> observer : observers)
  18. observer.added(this, element);
  19. }
  20. }
  21. @Override
  22. public boolean add(E element) {
  23. boolean added = super.add(element);
  24. if (added)
  25. notifyElementAdded(element);
  26. return added;
  27. }
  28. @Override
  29. public boolean addAll(Collection<? extends E> c) {
  30. boolean result = false;
  31. for (E element : c)
  32. result |= add(element); // Calls notifyElementAdded
  33. return result;
  34. }
  35. }

Observers subscribe to notifications by invoking the addObserver method and unsubscribe by invoking the removeObserver method. In both cases, an instance of this callback interface is passed to the method.

观察者通过调用 addObserver 方法订阅通知,通过调用 removeObserver 方法取消订阅。在这两种情况下,都会将此回调接口的实例传递给方法。

  1. @FunctionalInterface
  2. public interface SetObserver<E> {
  3. // Invoked when an element is added to the observable set
  4. void added(ObservableSet<E> set, E element);
  5. }

This interface is structurally identical to BiConsumer<ObservableSet<E>,E>. We chose to define a custom functional interface because the interface and method names make the code more readable and because the interface could evolve to incorporate multiple callbacks. That said, a reasonable argument could also be made for using BiConsumer (Item 44).

这个接口在结构上与 BiConsumer<ObservableSet<E>,E> 相同。我们选择定义一个自定义函数式接口,因为接口和方法名称使代码更具可读性,而且接口可以演化为包含多个回调。也就是说,使用 BiConsumer 也是合理的(Item-44)。

On cursory inspection, ObservableSet appears to work fine. For example, the following program prints the numbers from 0 through 99:

粗略地检查一下,ObservableSet 似乎工作得很好。例如,下面的程序打印从 0 到 99 的数字:

  1. public static void main(String[] args) {
  2. ObservableSet<Integer> set =new ObservableSet<>(new HashSet<>());
  3. set.addObserver((s, e) -> System.out.println(e));
  4. for (int i = 0; i < 100; i++)
  5. set.add(i);
  6. }

Now let’s try something a bit fancier. Suppose we replace the addObserver call with one that passes an observer that prints the Integer value that was added to the set and removes itself if the value is 23:

现在让我们尝试一些更奇特的东西。假设我们将 addObserver 调用替换为一个传递观察者的调用,该观察者打印添加到集合中的整数值,如果该值为 23,则该调用将删除自身:

  1. set.addObserver(new SetObserver<>() {
  2. public void added(ObservableSet<Integer> s, Integer e) {
  3. System.out.println(e);
  4. if (e == 23)
  5. s.removeObserver(this);
  6. }
  7. });

Note that this call uses an anonymous class instance in place of the lambda used in the previous call. That is because the function object needs to pass itself to s.removeObserver, and lambdas cannot access themselves (Item 42).

注意,这个调用使用一个匿名类实例来代替前面调用中使用的 lambda 表达式。这是因为函数对象需要将自己传递给 s.removeObserver,而 lambda 表达式不能访问自身(Item-42)。

You might expect the program to print the numbers 0 through 23, after which the observer would unsubscribe and the program would terminate silently. In fact, it prints these numbers and then throws a ConcurrentModificationException. The problem is that notifyElementAdded is in the process of iterating over the observers list when it invokes the observer’s added method. The added method calls the observable set’s removeObserver method, which in turn calls the method observers.remove. Now we’re in trouble. We are trying to remove an element from a list in the midst of iterating over it, which is illegal. The iteration in the notifyElementAdded method is in a synchronized block to prevent concurrent modification, but it doesn’t prevent the iterating thread itself from calling back into the observable set and modifying its observers list.

你可能希望程序打印数字 0 到 23,然后观察者将取消订阅,程序将无声地终止。实际上,它打印这些数字,然后抛出 ConcurrentModificationException。问题在于 notifyElementAdded 在调用观察者的 added 方法时,正在遍历 observers 列表。added 方法调用可观察集的 removeObserver 方法,该方法反过来调用方法 observers.remove。现在我们有麻烦了。我们试图在遍历列表的过程中从列表中删除一个元素,这是非法的。notifyElementAdded 方法中的迭代位于一个同步块中,以防止并发修改,但是无法防止迭代线程本身回调到可观察的集合中,也无法防止修改它的 observers 列表。

Now let’s try something odd: let’s write an observer that tries to unsubscribe, but instead of calling removeObserver directly, it engages the services of another thread to do the deed. This observer uses an executor service (Item 80):

现在让我们尝试一些奇怪的事情:让我们编写一个观察者来尝试取消订阅,但是它没有直接调用 removeObserver,而是使用另一个线程的服务来执行这个操作。该观察者使用 executor 服务(Item-80):

  1. // Observer that uses a background thread needlessly
  2. set.addObserver(new SetObserver<>() {
  3. public void added(ObservableSet<Integer> s, Integer e) {
  4. System.out.println(e);
  5. if (e == 23) {
  6. ExecutorService exec = Executors.newSingleThreadExecutor();
  7. try {
  8. exec.submit(() -> s.removeObserver(this)).get();
  9. } catch (ExecutionException | InterruptedException ex) {
  10. throw new AssertionError(ex);
  11. } finally {
  12. exec.shutdown();
  13. }
  14. }
  15. }
  16. });

Incidentally, note that this program catches two different exception types in one catch clause. This facility, informally known as multi-catch, was added in Java 7. It can greatly increase the clarity and reduce the size of programs that behave the same way in response to multiple exception types.

顺便提一下,注意这个程序在一个 catch 子句中捕获了两种不同的异常类型。这个功能在 Java 7 中添加了,非正式名称为 multi-catch。它可以极大地提高清晰度,并减少在响应多种异常类型时表现相同的程序的大小。

When we run this program, we don’t get an exception; we get a deadlock. The background thread calls s.removeObserver, which attempts to lock observers, but it can’t acquire the lock, because the main thread already has the lock. All the while, the main thread is waiting for the background thread to finish removing the observer, which explains the deadlock.

当我们运行这个程序时,我们不会得到异常;而是遭遇了死锁。后台线程调用 s.removeObserver,它试图锁定观察者,但无法获取锁,因为主线程已经拥有锁。一直以来,主线程都在等待后台线程完成删除观察者的操作,这就解释了死锁的原因。

This example is contrived because there is no reason for the observer to use a background thread to unsubscribe itself, but the problem is real. Invoking alien methods from within synchronized regions has caused many deadlocks in real systems, such as GUI toolkits.

这个例子是人为设计的,因为观察者没有理由使用后台线程来取消订阅本身,但是问题是真实的。在实际系统中,从同步区域内调用外来方法会导致许多死锁,比如 GUI 工具包。

In both of the previous examples (the exception and the deadlock) we were lucky. The resource that was guarded by the synchronized region (observers) was in a consistent state when the alien method (added) was invoked. Suppose you were to invoke an alien method from a synchronized region while the invariant protected by the synchronized region was temporarily invalid. Because locks in the Java programming language are reentrant, such calls won’t deadlock. As in the first example, which resulted in an exception, the calling thread already holds the lock, so the thread will succeed when it tries to reacquire the lock, even though another conceptually unrelated operation is in progress on the data guarded by the lock. The consequences of such a failure can be catastrophic. In essence, the lock has failed to do its job. Reentrant locks simplify the construction of multithreaded object-oriented programs, but they can turn liveness failures into safety failures.

在前面的两个例子中(异常和死锁),我们都很幸运。调用外来方法(added)时,由同步区域(观察者)保护的资源处于一致状态。假设你要从同步区域调用一个外来方法,而同步区域保护的不变量暂时无效。因为 Java 编程语言中的锁是可重入的,所以这样的调用不会死锁。与第一个导致异常的示例一样,调用线程已经持有锁,所以当它试图重新获得锁时,线程将成功,即使另一个概念上不相关的操作正在对锁保护的数据进行中。这种失败的后果可能是灾难性的。从本质上说,这把锁没能发挥它的作用。可重入锁简化了多线程面向对象程序的构造,但它们可以将活动故障转化为安全故障。

Luckily, it is usually not too hard to fix this sort of problem by moving alien method invocations out of synchronized blocks. For the notifyElementAdded method, this involves taking a “snapshot” of the observers list that can then be safely traversed without a lock. With this change, both of the previous examples run without exception or deadlock:

幸运的是,通过将外来方法调用移出同步块来解决这类问题通常并不难。对于 notifyElementAdded 方法,这涉及到获取观察者列表的「快照」,然后可以在没有锁的情况下安全地遍历该列表。有了这个改变,前面的两个例子都可以再也不会出现异常或者死锁了:

  1. // Alien method moved outside of synchronized block - open calls
  2. private void notifyElementAdded(E element) {
  3. List<SetObserver<E>> snapshot = null;
  4. synchronized(observers) {
  5. snapshot = new ArrayList<>(observers);
  6. }
  7. for (SetObserver<E> observer :snapshot)
  8. observer.added(this, element);
  9. }

In fact, there’s a better way to move the alien method invocations out of the synchronized block. The libraries provide a concurrent collection (Item 81) known as CopyOnWriteArrayList that is tailor-made for this purpose. This List implementation is a variant of ArrayList in which all modification operations are implemented by making a fresh copy of the entire underlying array. Because the internal array is never modified, iteration requires no locking and is very fast. For most uses, the performance of CopyOnWriteArrayList would be atrocious, but it’s perfect for observer lists, which are rarely modified and often traversed.

实际上,有一种更好的方法可以将外来方法调用移出同步块。库提供了一个名为 CopyOnWriteArrayList 的并发集合(Item-81),该集合是为此目的量身定制的。此列表实现是 ArrayList 的变体,其中所有修改操作都是通过复制整个底层数组来实现的。因为从不修改内部数组,所以迭代不需要锁定,而且速度非常快。如果大量使用,CopyOnWriteArrayList 的性能会很差,但是对于很少修改和经常遍历的观察者列表来说,它是完美的。

The add and addAll methods of ObservableSet need not be changed if the list is modified to use CopyOnWriteArrayList. Here is how the remainder of the class looks. Notice that there is no explicit synchronization whatsoever:

如果将 list 修改为使用 CopyOnWriteArrayList,则不需要更改 ObservableSet 的 add 和 addAll 方法。下面是类的其余部分。请注意,没有任何显式同步:

  1. // Thread-safe observable set with CopyOnWriteArrayList
  2. private final List<SetObserver<E>> observers =new CopyOnWriteArrayList<>();
  3. public void addObserver(SetObserver<E> observer) {
  4. observers.add(observer);
  5. }
  6. public boolean removeObserver(SetObserver<E> observer) {
  7. return observers.remove(observer);
  8. }
  9. private void notifyElementAdded(E element) {
  10. for (SetObserver<E> observer : observers)
  11. observer.added(this, element);
  12. }

An alien method invoked outside of a synchronized region is known as an open call [Goetz06, 10.1.4]. Besides preventing failures, open calls can greatly increase concurrency. An alien method might run for an arbitrarily long period. If the alien method were invoked from a synchronized region, other threads would be denied access to the protected resource unnecessarily.

在同步区域之外调用的外来方法称为 open call [Goetz06, 10.1.4]。除了防止失败之外,开放调用还可以极大地提高并发性。一个陌生的方法可以运行任意长的时间。如果从同步区域调用了外来方法,其他线程对受保护资源的访问就会遭到不必要的拒绝。

As a rule, you should do as little work as possible inside synchronized regions. Obtain the lock, examine the shared data, transform it as necessary, and drop the lock. If you must perform some time-consuming activity, find a way to move it out of the synchronized region without violating the guidelines in Item 78.

作为规则,你应该在同步区域内做尽可能少的工作。 获取锁,检查共享数据,根据需要进行转换,然后删除锁。如果你必须执行一些耗时的活动,请设法将其移出同步区域,而不违反 Item-78 中的指导原则。

The first part of this item was about correctness. Now let’s take a brief look at performance. While the cost of synchronization has plummeted since the early days of Java, it is more important than ever not to oversynchronize. In a multicore world, the real cost of excessive synchronization is not the CPU time spent getting locks; it is contention: the lost opportunities for parallelism and the delays imposed by the need to ensure that every core has a consistent view of memory. Another hidden cost of oversynchronization is that it can limit the VM’s ability to optimize code execution.

本条目的第一部分是关于正确性的。现在让我们简要地看一下性能。虽然自 Java 早期以来,同步的成本已经大幅下降,但比以往任何时候都更重要的是:不要过度同步。在多核世界中,过度同步的真正代价不是获得锁所花费的 CPU 时间;这是一种争论:而是失去了并行化的机会,以及由于需要确保每个核心都有一个一致的内存视图而造成的延迟。过度同步的另一个隐藏成本是,它可能限制 VM 优化代码执行的能力。

If you are writing a mutable class, you have two options: you can omit all synchronization and allow the client to synchronize externally if concurrent use is desired, or you can synchronize internally, making the class thread-safe (Item 82). You should choose the latter option only if you can achieve significantly higher concurrency with internal synchronization than you could by having the client lock the entire object externally. The collections in java.util (with the exception of the obsolete Vector and Hashtable) take the former approach, while those in java.util.concurrent take the latter (Item 81).

如果你正在编写一个可变的类,你有两个选择:你可以省略所有同步并允许客户端在需要并发使用时在外部进行同步,或者你可以在内部进行同步,从而使类是线程安全的(Item-82)。只有当你能够通过内部同步实现比通过让客户端在外部锁定整个对象获得高得多的并发性时,才应该选择后者。java.util 中的集合(废弃的 Vector 和 Hashtable 除外)采用前一种方法,而 java.util.concurrent 中的方法则采用后者(Item-81)。

In the early days of Java, many classes violated these guidelines. For example, StringBuffer instances are almost always used by a single thread, yet they perform internal synchronization. It is for this reason that StringBuffer was supplanted by StringBuilder, which is just an unsynchronized StringBuffer. Similarly, it’s a large part of the reason that the thread-safe pseudorandom number generator in java.util.Random was supplanted by the unsynchronized implementation in java.util.concurrent.ThreadLocalRandom. When in doubt, do not synchronize your class, but document that it is not thread-safe.

在 Java 的早期,许多类违反了这些准则。例如,StringBuffer 实例几乎总是由一个线程使用,但是它们执行内部同步。正是由于这个原因,StringBuffer 被 StringBuilder 取代,而 StringBuilder 只是一个未同步的 StringBuffer。类似地,同样,java.util.Random 中的线程安全伪随机数生成器被 java.util.concurrent.ThreadLocalRandom 中的非同步实现所取代,这也是原因之一。如果有疑问,不要同步你的类,但要记录它不是线程安全的。

If you do synchronize your class internally, you can use various techniques to achieve high concurrency, such as lock splitting, lock striping, and nonblocking concurrency control. These techniques are beyond the scope of this book, but they are discussed elsewhere [Goetz06, Herlihy08].

如果你在内部同步你的类,你可以使用各种技术来实现高并发性,例如分拆锁、分离锁和非阻塞并发控制。这些技术超出了本书的范围,但是在其他地方也有讨论 [Goetz06, Herlihy08]。

If a method modifies a static field and there is any possibility that the method will be called from multiple threads, you must synchronize access to the field internally (unless the class can tolerate nondeterministic behavior). It is not possible for a multithreaded client to perform external synchronization on such a method, because unrelated clients can invoke the method without synchronization. The field is essentially a global variable even if it is private because it can be read and modified by unrelated clients. The nextSerialNumber field used by the method generateSerialNumber in Item 78 exemplifies this situation.

如果一个方法修改了一个静态字段,并且有可能从多个线程调用该方法,则必须在内部同步对该字段的访问(除非该类能够容忍不确定性行为)。多线程客户端不可能对这样的方法执行外部同步,因为不相关的客户端可以在不同步的情况下调用该方法。字段本质上是一个全局变量,即使它是私有的,因为它可以被不相关的客户端读取和修改。Item-78 中的 generateSerialNumber 方法使用的 nextSerialNumber 字段演示了这种情况。

In summary, to avoid deadlock and data corruption, never call an alien method from within a synchronized region. More generally, keep the amount of work that you do from within synchronized regions to a minimum. When you are designing a mutable class, think about whether it should do its own synchronization. In the multicore era, it is more important than ever not to oversynchronize. Synchronize your class internally only if there is a good reason to do so, and document your decision clearly (Item 82).

总之,为了避免死锁和数据损坏,永远不要从同步区域内调用外来方法。更一般地说,将你在同步区域内所做的工作量保持在最小。在设计可变类时,请考虑它是否应该执行自己的同步。在多核时代,比以往任何时候都更重要的是不要过度同步。只有在有充分理由时,才在内部同步类,并清楚地记录你的决定(Item-82)。


【80】Executor、task、流优于直接使用线程

Prefer executors, tasks, and streams to threads

The first edition of this book contained code for a simple work queue [Bloch01, Item 49]. This class allowed clients to enqueue work for asynchronous processing by a background thread. When the work queue was no longer needed, the client could invoke a method to ask the background thread to terminate itself gracefully after completing any work that was already on the queue. The implementation was little more than a toy, but even so, it required a full page of subtle, delicate code, of the sort that is prone to safety and liveness failures if you don’t get it just right. Luckily, there is no reason to write this sort of code anymore.

本书的第一版包含一个简单工作队列的代码 [Bloch01, Item 49]。这个类允许客户端通过后台线程为异步处理排队。当不再需要工作队列时,客户端可以调用一个方法,要求后台线程在完成队列上的任何工作后优雅地终止自己。这个实现只不过是一个玩具,但即便如此,它也需要一整页的代码,如果你做得不对,就很容易出现安全和活性失败。幸运的是,没有理由再编写这种代码了。

By the time the second edition of this book came out, java.util.concurrent had been added to Java. This package contains an Executor Framework, which is a flexible interface-based task execution facility. Creating a work queue that is better in every way than the one in the first edition of this book requires but a single line of code:

当这本书的第二版出版时,java.util.concurrent 已经添加到 Java 中。这个包有一个 Executor 框架,它是一个灵活的基于接口的任务执行工具。创建一个工作队列,它在任何方面都比在这本书的第一版更好,只需要一行代码:

  1. ExecutorService exec = Executors.newSingleThreadExecutor();
  2. Here is how to submit a runnable for execution:
  3. exec.execute(runnable);
  4. And here is how to tell the executor to terminate gracefully (if you fail to do this,it is likely that your VM will not exit):
  5. exec.shutdown();

You can do many more things with an executor service. For example, you can wait for a particular task to complete (with the get method, as shown in Item 79, page 319), you can wait for any or all of a collection of tasks to complete (using the invokeAny or invokeAll methods), you can wait for the executor service to terminate (using the awaitTermination method), you can retrieve the results of tasks one by one as they complete (using an ExecutorCompletionService), you can schedule tasks to run at a particular time or to run periodically (using a ScheduledThreadPoolExecutor), and so on.

你可以使用 executor 服务做更多的事情。例如,你可以等待一个特定任务完成(使用 get 方法,参见 Item-79,319 页),你可以等待任务集合中任何或全部任务完成(使用 invokeAny 或 invokeAll 方法),你可以等待 executor 服务终止(使用 awaitTermination 方法),你可以一个接一个检索任务,获取他们完成的结果(使用一个 ExecutorCompletionService),还可以安排任务在特定时间运行或定期运行(使用 ScheduledThreadPoolExecutor),等等。

If you want more than one thread to process requests from the queue, simply call a different static factory that creates a different kind of executor service called a thread pool. You can create a thread pool with a fixed or variable number of threads. The java.util.concurrent.Executors class contains static factories that provide most of the executors you’ll ever need. If, however, you want something out of the ordinary, you can use the ThreadPoolExecutor class directly. This class lets you configure nearly every aspect of a thread pool’s operation.

如果希望多个线程处理来自队列的请求,只需调用一个不同的静态工厂,该工厂创建一种称为线程池的不同类型的 executor 服务。你可以使用固定或可变数量的线程创建线程池。java.util.concurrent.Executors 类包含静态工厂,它们提供你需要的大多数 executor。但是,如果你想要一些不同寻常的东西,你可以直接使用 ThreadPoolExecutor 类。这个类允许你配置线程池操作的几乎每个方面。

Choosing the executor service for a particular application can be tricky. For a small program, or a lightly loaded server, Executors.newCachedThreadPool is generally a good choice because it demands no configuration and generally “does the right thing.” But a cached thread pool is not a good choice for a heavily loaded production server! In a cached thread pool, submitted tasks are not queued but immediately handed off to a thread for execution. If no threads are available, a new one is created. If a server is so heavily loaded that all of its CPUs are fully utilized and more tasks arrive, more threads will be created, which will only make matters worse. Therefore, in a heavily loaded production server, you are much better off using Executors.newFixedThreadPool, which gives you a pool with a fixed number of threads, or using the ThreadPoolExecutor class directly, for maximum control.

为特定的应用程序选择 executor 服务可能比较棘手。对于小程序或负载较轻的服务器,Executors.newCachedThreadPool 通常是一个不错的选择,因为它不需要配置,而且通常「做正确的事情」。但是对于负载沉重的生产服务器来说,缓存的线程池不是一个好的选择!在缓存的线程池中,提交的任务不会排队,而是立即传递给线程执行。如果没有可用的线程,则创建一个新的线程。如果服务器负载过重,所有 CPU 都被充分利用,并且有更多的任务到达,就会创建更多的线程,这只会使情况变得更糟。因此,在负载沉重的生产服务器中,最好使用 Executors.newFixedThreadPool,它为你提供一个线程数量固定的池,或者直接使用 ThreadPoolExecutor 类来实现最大限度的控制。

Not only should you refrain from writing your own work queues, but you should generally refrain from working directly with threads. When you work directly with threads, a Thread serves as both a unit of work and the mechanism for executing it. In the executor framework, the unit of work and the execution mechanism are separate. The key abstraction is the unit of work, which is the task. There are two kinds of tasks: Runnable and its close cousin, Callable (which is like Runnable, except that it returns a value and can throw arbitrary exceptions). The general mechanism for executing tasks is the executor service. If you think in terms of tasks and let an executor service execute them for you, you gain the flexibility to select an appropriate execution policy to meet your needs and to change the policy if your needs change. In essence, the Executor Framework does for execution what the Collections Framework did for aggregation.

你不仅应该避免编写自己的工作队列,而且通常还应该避免直接使用线程。当你直接使用线程时,线程既是工作单元,又是执行它的机制。在 executor 框架中,工作单元和执行机制是分开的。关键的抽象是工作单元,即任务。有两种任务:Runnable 和它的近亲 Callable(与 Runnable 类似,只是它返回一个值并可以抛出任意异常)。执行任务的一般机制是 executor 服务。如果你从任务的角度考虑问题,并让 executor 服务为你执行这些任务,那么你就可以灵活地选择合适的执行策略来满足你的需求,并在你的需求发生变化时更改策略。本质上,Executor 框架执行的功能与 Collections 框架聚合的功能相同。

In Java 7, the Executor Framework was extended to support fork-join tasks, which are run by a special kind of executor service known as a fork-join pool. A fork-join task, represented by a ForkJoinTask instance, may be split up into smaller subtasks, and the threads comprising a ForkJoinPool not only process these tasks but “steal” tasks from one another to ensure that all threads remain busy, resulting in higher CPU utilization, higher throughput, and lower latency. Writing and tuning fork-join tasks is tricky. Parallel streams (Item 48) are written atop fork join pools and allow you to take advantage of their performance benefits with little effort, assuming they are appropriate for the task at hand.

在 Java 7 中,Executor 框架被扩展为支持 fork-join 任务,这些任务由一种特殊的 Executor 服务(称为 fork-join 池)运行。由 ForkJoinTask 实例表示的 fork-join 任务可以划分为更小的子任务,由 ForkJoinPool 组成的线程不仅处理这些任务,而且还从其他线程「窃取」任务,以确保所有线程都处于繁忙状态,从而提高 CPU 利用率、更高的吞吐量和更低的延迟。编写和调优 fork-join 任务非常棘手。并行流(Item-48) 是在 fork 连接池之上编写的,假设它们适合当前的任务,那么你可以轻松地利用它们的性能优势。

A complete treatment of the Executor Framework is beyond the scope of this book, but the interested reader is directed to Java Concurrency in Practice [Goetz06].

对 Executor 框架的完整处理超出了本书的范围,但是感兴趣的读者可以在实践中可以参阅《Java Concurrency in Practice》 [Goetz06]。


【81】并发实用工具优于 wait 和 notify

Prefer concurrency utilities to wait and notify

The first edition of this book devoted an item to the correct use of wait and notify [Bloch01, Item 50]. Its advice is still valid and is summarized at end of this item, but this advice is far less important than it once was. This is because there is far less reason to use wait and notify. Since Java 5, the platform has provided higher-level concurrency utilities that do the sorts of things you formerly had to hand-code atop wait and notify. Given the difficulty of using wait and notify correctly, you should use the higher-level concurrency utilities instead.

这本书的第一版专门介绍了 wait 和 notify 的正确用法 [Bloch01, item 50]。这些建议仍然有效,并在本条目末尾作了总结,但这一建议已远不如从前重要。这是因为使用 wait 和 notify 的理由要少得多。自 Java 5 以来,该平台提供了更高级别的并发实用工具,可以执行以前必须在 wait 和 notify 上手工编写代码的操作。考虑到正确使用 wait 和 notify 的困难,你应该使用更高级别的并发实用工具。

The higher-level utilities in java.util.concurrent fall into three categories: the Executor Framework, which was covered briefly in Item 80; concurrent collections; and synchronizers. Concurrent collections and synchronizers are covered briefly in this item.

java.util.concurrent 中级别较高的实用工具可分为三类:Executor 框架,Item-80 简要介绍了该框架;并发集合;同步器。本条目简要介绍并发集合和同步器。

The concurrent collections are high-performance concurrent implementations of standard collection interfaces such as List, Queue, and Map. To provide high concurrency, these implementations manage their own synchronization internally (Item 79). Therefore, it is impossible to exclude concurrent activity from a concurrent collection; locking it will only slow the program.

并发集合是标准集合接口,如 List、Queue 和 Map 的高性能并发实现。为了提供高并发性,这些实现在内部管理它们自己的同步(Item-79)。因此,不可能从并发集合中排除并发活动;锁定它只会使程序变慢。

Because you can’t exclude concurrent activity on concurrent collections, you can’t atomically compose method invocations on them either. Therefore, concurrent collection interfaces were outfitted with state-dependent modify operations, which combine several primitives into a single atomic operation. These operations proved sufficiently useful on concurrent collections that they were added to the corresponding collection interfaces in Java 8, using default methods (Item 21).

因为不能排除并发集合上的并发活动,所以也不能原子地组合对它们的方法调用。因此,并发集合接口配备了依赖于状态的修改操作,这些操作将多个基本操作组合成单个原子操作。这些操作在并发集合上非常有用,因此使用默认方法(Item-21)将它们添加到 Java 8 中相应的集合接口。

For example, Map’s putIfAbsent(key, value) method inserts a mapping for a key if none was present and returns the previous value associated with the key, or null if there was none. This makes it easy to implement thread-safe canonicalizing maps. This method simulates the behavior of String.intern:

例如,Map 的 putIfAbsent(key, value) 方法为一个没有映射的键插入一个映射,并返回与键关联的前一个值,如果没有,则返回 null。这使得实现线程安全的规范化 Map 变得很容易。这个方法模拟了 String.intern 的行为。

  1. // Concurrent canonicalizing map atop ConcurrentMap - not optimal
  2. private static final ConcurrentMap<String, String> map =new ConcurrentHashMap<>();
  3. public static String intern(String s) {
  4. String previousValue = map.putIfAbsent(s, s);
  5. return previousValue == null ? s : previousValue;
  6. }

In fact, you can do even better. ConcurrentHashMap is optimized for retrieval operations, such as get. Therefore, it is worth invoking get initially and calling putIfAbsent only if get indicates that it is necessary:

事实上,你可以做得更好。ConcurrentHashMap 针对 get 等检索操作进行了优化。因此,只有在 get 表明有必要时,才值得首先调用 get 再调用 putIfAbsent:

  1. // Concurrent canonicalizing map atop ConcurrentMap - faster!
  2. public static String intern(String s) {
  3. String result = map.get(s);
  4. if (result == null) {
  5. result = map.putIfAbsent(s, s);
  6. if (result == null)
  7. result = s;
  8. }
  9. return result;
  10. }

Besides offering excellent concurrency, ConcurrentHashMap is very fast. On my machine, the intern method above is over six times faster than String.intern (but keep in mind that String.intern must employ some strategy to keep from leaking memory in a long-lived application). Concurrent collections make synchronized collections largely obsolete. For example, use ConcurrentHashMap in preference to Collections.synchronizedMap. Simply replacing synchronized maps with concurrent maps can dramatically increase the performance of concurrent applications.

除了提供优秀的并发性,ConcurrentHashMap 还非常快。在我的机器上,上面的 intern 方法比 String.intern 快六倍多(但是请记住,String.intern 必须使用一些策略来防止在长时间运行的应用程序中内存泄漏)。并发集合使同步集合在很大程度上过时。例如,使用 ConcurrentHashMap 而不是 Collections.synchronizedMap 只要用并发 Map 替换同步 Map 就可以显著提高并发应用程序的性能。

Some of the collection interfaces were extended with blocking operations, which wait (or block) until they can be successfully performed. For example, BlockingQueue extends Queue and adds several methods, including take, which removes and returns the head element from the queue, waiting if the queue is empty. This allows blocking queues to be used for work queues (also known as producer-consumer queues), to which one or more producer threads enqueue work items and from which one or more consumer threads dequeue and process items as they become available. As you’d expect, most ExecutorService implementations, including ThreadPoolExecutor, use a BlockingQueue (Item 80).

一些集合接口使用阻塞操作进行了扩展,这些操作将等待(或阻塞)成功执行。例如,BlockingQueue 扩展了 Queue 并添加了几个方法,包括 take,它从队列中删除并返回首个元素,如果队列为空,则等待。这允许将阻塞队列用于工作队列(也称为生产者-消费者队列),一个或多个生产者线程将工作项添加到该工作队列中,一个或多个消费者线程将工作项从该工作队列中取出并在这些工作项可用时处理它们。正如你所期望的,大多数 ExecutorService 实现,包括 ThreadPoolExecutor,都使用 BlockingQueue(Item-80)。

Synchronizers are objects that enable threads to wait for one another, allowing them to coordinate their activities. The most commonly used synchronizers are CountDownLatch and Semaphore. Less commonly used are CyclicBarrier and Exchanger. The most powerful synchronizer is Phaser.

同步器是允许线程彼此等待的对象,允许它们协调各自的活动。最常用的同步器是 CountDownLatch 和 Semaphore。较不常用的是 CyclicBarrier 和 Exchanger。最强大的同步器是 Phaser。

Countdown latches are single-use barriers that allow one or more threads to wait for one or more other threads to do something. The sole constructor for CountDownLatch takes an int that is the number of times the countDown method must be invoked on the latch before all waiting threads are allowed to proceed.

Countdown latches are single-use barriers,允许一个或多个线程等待一个或多个其他线程执行某些操作。CountDownLatch 的惟一构造函数接受一个 int,这个 int 是在允许所有等待的线程继续之前,必须在锁存器上调用倒计时方法的次数。

It is surprisingly easy to build useful things atop this simple primitive. For example, suppose you want to build a simple framework for timing the concurrent execution of an action. This framework consists of a single method that takes an executor to execute the action, a concurrency level representing the number of actions to be executed concurrently, and a runnable representing the action. All of the worker threads ready themselves to run the action before the timer thread starts the clock. When the last worker thread is ready to run the action, the timer thread “fires the starting gun,” allowing the worker threads to perform the action. As soon as the last worker thread finishes performing the action, the timer thread stops the clock. Implementing this logic directly on top of wait and notify would be messy to say the least, but it is surprisingly straightforward on top of CountDownLatch:

在这个简单的基本类型上构建有用的东西非常容易。例如,假设你想要构建一个简单的框架来为一个操作的并发执行计时。这个框架由一个方法组成,该方法使用一个 executor 来执行操作,一个并发级别表示要并发执行的操作的数量,一个 runnable 表示操作。所有工作线程都准备在 timer 线程启动时钟之前运行操作。当最后一个工作线程准备好运行该操作时,计时器线程「发令枪」,允许工作线程执行该操作。一旦最后一个工作线程完成该操作,计时器线程就停止时钟。在 wait 和 notify 的基础上直接实现这种逻辑至少会有点麻烦,但是在 CountDownLatch 的基础上实现起来却非常简单:

  1. // Simple framework for timing concurrent execution
  2. public static long time(Executor executor, int concurrency,Runnable action) throws InterruptedException {
  3. CountDownLatch ready = new CountDownLatch(concurrency);
  4. CountDownLatch start = new CountDownLatch(1);
  5. CountDownLatch done = new CountDownLatch(concurrency);
  6. for (int i = 0; i < concurrency; i++) {
  7. executor.execute(() -> {
  8. ready.countDown(); // Tell timer we're ready
  9. try {
  10. start.await(); // Wait till peers are ready
  11. action.run();
  12. } catch (InterruptedException e) {
  13. Thread.currentThread().interrupt();
  14. } finally {
  15. done.countDown(); // Tell timer we're done
  16. }
  17. });
  18. }
  19. ready.await(); // Wait for all workers to be ready
  20. long startNanos = System.nanoTime();
  21. start.countDown(); // And they're off!
  22. done.await(); // Wait for all workers to finish
  23. return System.nanoTime() - startNanos;
  24. }

Note that the method uses three countdown latches. The first, ready, is used by worker threads to tell the timer thread when they’re ready. The worker threads then wait on the second latch, which is start. When the last worker thread invokes ready.countDown, the timer thread records the start time and invokes start.countDown, allowing all of the worker threads to proceed. Then the timer thread waits on the third latch, done, until the last of the worker threads finishes running the action and calls done.countDown. As soon as this happens, the timer thread awakens and records the end time.

注意,该方法使用三个倒计时锁。第一个是 ready,工作线程使用它来告诉 timer 线程它们什么时候准备好了。工作线程然后等待第二个锁存器,即 start。当最后一个工作线程调用 ready.countDown 时。timer 线程记录开始时间并调用 start.countDown,允许所有工作线程继续。然后计时器线程等待第三个锁存器 done,直到最后一个工作线程运行完操作并调用 done.countDown。一旦发生这种情况,timer 线程就会唤醒并记录结束时间。

A few more details bear noting. The executor passed to the time method must allow for the creation of at least as many threads as the given concurrency level, or the test will never complete. This is known as a thread starvation deadlock [Goetz06, 8.1.1]. If a worker thread catches an InterruptedException, it reasserts the interrupt using the idiom Thread.currentThread().interrupt() and returns from its run method. This allows the executor to deal with the interrupt as it sees fit. Note that System.nanoTime is used to time the activity. For interval timing, always use System.nanoTime rather than System.currentTimeMillis. System.nanoTime is both more accurate and more precise and is unaffected by adjustments to the system’s realtime clock. Finally, note that the code in this example won’t yield accurate timings unless action does a fair amount of work, say a second or more. Accurate microbenchmarking is notoriously hard and is best done with the aid of a specialized framework such as jmh [JMH].

还有一些细节值得注意。传递给 time 方法的 executor 必须允许创建至少与给定并发级别相同数量的线程,否则测试将永远不会完成。这被称为线程饥饿死锁 [Goetz06, 8.1.1]。如果工作线程捕捉到 InterruptedException,它使用习惯用法 Thread.currentThread().interrupt() 重申中断,并从它的 run 方法返回。这允许执行程序按照它认为合适的方式处理中断。请注意,System.nanoTime 是用来计时的。对于间隔计时,始终使用 System.nanoTime 而不是 System.currentTimeMillis System.nanoTime 不仅更准确,而且更精确,而且不受系统实时时钟调整的影响。最后,请注意,本例中的代码不会产生准确的计时,除非 action 做了相当多的工作,比如一秒钟或更长时间。准确的微基准测试是出了名的困难,最好是借助诸如 jmh 这样的专业框架来完成。

This item only scratches the surface of what you can do with the concurrency utilities. For example, the three countdown latches in the previous example could be replaced by a single CyclicBarrier or Phaser instance. The resulting code would be a bit more concise but perhaps more difficult to understand.

本条目只涉及到你可以使用并发实用工具做什么。例如,前面示例中的三个倒计时锁存器可以替换为单个 CyclicBarrier 或 Phaser 实例。生成的代码可能更简洁,但可能更难于理解。

While you should always use the concurrency utilities in preference to wait and notify, you might have to maintain legacy code that uses wait and notify. The wait method is used to make a thread wait for some condition. It must be invoked inside a synchronized region that locks the object on which it is invoked. Here is the standard idiom for using the wait method:

虽然你应该始终优先使用并发实用工具,而不是使用 wait 和 notify,但是你可能必须维护使用 wait 和 notify 的遗留代码。wait 方法用于使线程等待某个条件。它必须在同步区域内调用,该同步区域将锁定调用它的对象。下面是使用 wait 方法的标准形式:

  1. // The standard idiom for using the wait method
  2. synchronized (obj) {
  3. while (<condition does not hold>)
  4. obj.wait(); // (Releases lock, and reacquires on wakeup)
  5. ... // Perform action appropriate to condition
  6. }

Always use the wait loop idiom to invoke the wait method; never invoke it outside of a loop. The loop serves to test the condition before and after waiting.

始终使用 wait 习惯用法,即循环来调用 wait 方法;永远不要在循环之外调用它。 循环用于在等待之前和之后测试条件。

Testing the condition before waiting and skipping the wait if the condition already holds are necessary to ensure liveness. If the condition already holds and the notify (or notifyAll) method has already been invoked before a thread waits, there is no guarantee that the thread will ever wake from the wait.

在等待之前测试条件,如果条件已经存在,则跳过等待,以确保活性。如果条件已经存在,并且在线程等待之前已经调用了 notify(或 notifyAll)方法,则不能保证线程将从等待中唤醒。

Testing the condition after waiting and waiting again if the condition does not hold are necessary to ensure safety. If the thread proceeds with the action when the condition does not hold, it can destroy the invariant guarded by the lock. There are several reasons a thread might wake up when the condition does not hold:

为了确保安全,需要在等待之后再测试条件,如果条件不成立,则再次等待。如果线程在条件不成立的情况下继续执行该操作,它可能会破坏由锁保护的不变性。当条件不成立时,有一些理由唤醒线程:

  • Another thread could have obtained the lock and changed the guarded state between the time a thread invoked notify and the waiting thread woke up.

另一个线程可以获得锁,并在线程调用 notify 和等待线程醒来之间更改保护状态。

  • Another thread could have invoked notify accidentally or maliciously when the condition did not hold. Classes expose themselves to this sort of mischief by waiting on publicly accessible objects. Any wait in a synchronized method of a publicly accessible object is susceptible to this problem.

当条件不成立时,另一个线程可能意外地或恶意地调用 notify。类通过等待公共可访问的对象来暴露自己。公共可访问对象的同步方法中的任何 wait 都容易受到这个问题的影响。

  • The notifying thread could be overly “generous” in waking waiting threads. For example, the notifying thread might invoke notifyAll even if only some of the waiting threads have their condition satisfied.

通知线程在唤醒等待线程时可能过于「慷慨」。例如,即使只有一些等待线程的条件得到满足,通知线程也可能调用 notifyAll。

  • The waiting thread could (rarely) wake up in the absence of a notify. This is known as a spurious wakeup [POSIX, 11.4.3.6.1; Java9-api].

在没有通知的情况下,等待的线程可能(很少)醒来。这被称为伪唤醒 [POSIX, 11.4.3.6.1; Java9-api]。

A related issue is whether to use notify or notifyAll to wake waiting threads. (Recall that notify wakes a single waiting thread, assuming such a thread exists, and notifyAll wakes all waiting threads.) It is sometimes said that you should always use notifyAll. This is reasonable, conservative advice. It will always yield correct results because it guarantees that you’ll wake the threads that need to be awakened. You may wake some other threads, too, but this won’t affect the correctness of your program. These threads will check the condition for which they’re waiting and, finding it false, will continue waiting.

一个相关的问题是,是使用 notify 还是 notifyAll 来唤醒等待的线程。(回想一下 notify 唤醒一个等待线程,假设存在这样一个线程,notifyAll 唤醒所有等待线程)。有时人们会说,应该始终使用 notifyAll。这是合理的、保守的建议。它总是会产生正确的结果,因为它保证你将唤醒需要唤醒的线程。你可能还会唤醒其他一些线程,但这不会影响程序的正确性。这些线程将检查它们正在等待的条件,如果发现为条件不满足,将继续等待。

As an optimization, you may choose to invoke notify instead of notifyAll if all threads that could be in the wait-set are waiting for the same condition and only one thread at a time can benefit from the condition becoming true.

作为一种优化,如果在等待状态的所有线程都在等待相同的条件,并且每次只有一个线程可以从条件中获益,那么你可以选择调用 notify 而不是 notifyAll。

Even if these preconditions are satisfied, there may be cause to use notifyAll in place of notify. Just as placing the wait invocation in a loop protects against accidental or malicious notifications on a publicly accessible object, using notifyAll in place of notify protects against accidental or malicious waits by an unrelated thread. Such waits could otherwise “swallow” a critical notification, leaving its intended recipient waiting indefinitely.

即使满足了这些先决条件,也可能有理由使用 notifyAll 来代替 notify。正如将 wait 调用放在循环中可以防止公共访问对象上的意外或恶意通知一样,使用 notifyAll 代替 notify 可以防止不相关线程的意外或恶意等待。否则,这样的等待可能会「吞下」一个关键通知,让预期的接收者无限期地等待。

In summary, using wait and notify directly is like programming in “concurrency assembly language,” as compared to the higher-level language provided by java.util.concurrent. There is seldom, if ever, a reason to use wait and notify in new code. If you maintain code that uses wait and notify, make sure that it always invokes wait from within a while loop using the standard idiom. The notifyAll method should generally be used in preference to notify. If notify is used, great care must be taken to ensure liveness.

总之,与 java.util.concurrent 提供的高级语言相比,直接使用 wait 和 notify 就像使用「并发汇编语言」编程一样原始。在新代码中很少有理由使用 wait 和 notify。 如果维护使用 wait 和 notify 的代码,请确保它始终使用标准的习惯用法,即在 while 循环中调用 wait。另外,notifyAll 方法通常应该优先于 notify。如果使用 notify,则必须非常小心以确保其活性。


【82】文档应包含线程安全属性

Document thread safety

How a class behaves when its methods are used concurrently is an important part of its contract with its clients. If you fail to document this aspect of a class’s behavior, its users will be forced to make assumptions. If these assumptions are wrong, the resulting program may perform insufficient synchronization (Item 78) or excessive synchronization (Item 79). In either case, serious errors may result.

类在其方法并发使用时的行为是其与客户端约定的重要组成部分。如果你没有记录类在这一方面的行为,那么它的用户将被迫做出假设。如果这些假设是错误的,生成的程序可能缺少足够的同步(Item-78)或过度的同步(Item-79)。无论哪种情况,都可能导致严重的错误。

You may hear it said that you can tell if a method is thread-safe by looking for the synchronized modifier in its documentation. This is wrong on several counts. In normal operation, Javadoc does not include the synchronized modifier in its output, and with good reason. The presence of the synchronized modifier in a method declaration is an implementation detail, not a part of its API. It does not reliably indicate that a method is thread-safe.

你可能听说过,可以通过在方法的文档中查找 synchronized 修饰符来判断方法是否线程安全。这个观点有好些方面是错误的。在正常操作中,Javadoc 的输出中没有包含同步修饰符,这是有原因的。方法声明中 synchronized 修饰符的存在是实现细节,而不是其 API 的一部分。它不能可靠地表明方法是线程安全的。

Moreover, the claim that the presence of the synchronized modifier is sufficient to document thread safety embodies the misconception that thread safety is an all-or-nothing property. In fact, there are several levels of thread safety. To enable safe concurrent use, a class must clearly document what level of thread safety it supports. The following list summarizes levels of thread safety. It is not exhaustive but covers the common cases:

此外,声称 synchronized 修饰符的存在就足以记录线程安全性,这个观点是对线程安全性属性的误解,认为要么全有要么全无。实际上,线程安全有几个级别。要启用安全的并发使用,类必须清楚地记录它支持的线程安全级别。 下面的列表总结了线程安全级别。它并非详尽无遗,但涵盖以下常见情况:

  • Immutable —Instances of this class appear constant. No external synchronization is necessary. Examples include String, Long, and BigInteger (Item 17).

不可变的。这个类的实例看起来是常量。不需要外部同步。示例包括 String、Long 和 BigInteger(Item-17)。

  • Unconditionally thread-safe —Instances of this class are mutable, but the class has sufficient internal synchronization that its instances can be used concurrently without the need for any external synchronization. Examples include AtomicLong and ConcurrentHashMap.

无条件线程安全。该类的实例是可变的,但是该类具有足够的内部同步,因此无需任何外部同步即可并发地使用该类的实例。例如 AtomicLong 和 ConcurrentHashMap。

  • Conditionally thread-safe —Like unconditionally thread-safe, except that some methods require external synchronization for safe concurrent use. Examples include the collections returned by the Collections.synchronized wrappers, whose iterators require external synchronization.

有条件的线程安全。与无条件线程安全类似,只是有些方法需要外部同步才能安全并发使用。示例包括 Collections.synchronized 包装器返回的集合,其迭代器需要外部同步。

  • Not thread-safe —Instances of this class are mutable. To use them concurrently, clients must surround each method invocation (or invocation sequence) with external synchronization of the clients’ choosing. Examples include the general-purpose collection implementations, such as ArrayList and HashMap.

非线程安全。该类的实例是可变的。要并发地使用它们,客户端必须使用外部同步来包围每个方法调用(或调用序列)。这样的例子包括通用的集合实现,例如 ArrayList 和 HashMap。

  • Thread-hostile —This class is unsafe for concurrent use even if every method invocation is surrounded by external synchronization. Thread hostility usually results from modifying static data without synchronization. No one writes a thread-hostile class on purpose; such classes typically result from the failure to consider concurrency. When a class or method is found to be thread-hostile, it is typically fixed or deprecated. The generateSerialNumber method in Item 78 would be thread-hostile in the absence of internal synchronization, as discussed on page 322.

线程对立。即使每个方法调用都被外部同步包围,该类对于并发使用也是不安全的。线程对立通常是由于在不同步的情况下修改静态数据而导致的。没有人故意编写线程对立类;此类通常是由于没有考虑并发性而导致的。当发现类或方法与线程不相容时,通常将其修复或弃用。Item-78 中的 generateSerialNumber 方法在没有内部同步的情况下是线程对立的,如第 322 页所述。

These categories (apart from thread-hostile) correspond roughly to the thread safety annotations in Java Concurrency in Practice, which are Immutable, ThreadSafe, and NotThreadSafe [Goetz06, Appendix A]. The unconditionally and conditionally thread-safe categories in the above taxonomy are both covered under the ThreadSafe annotation.

这些类别(不包括线程对立类)大致对应于《Java Concurrency in Practice》中的线程安全注解,分别为 Immutable、ThreadSafe 和 NotThreadSafe [Goetz06, Appendix A]。上面分类中的无条件线程安全和有条件的线程安全都包含在 ThreadSafe 注解中。

Documenting a conditionally thread-safe class requires care. You must indicate which invocation sequences require external synchronization, and which lock (or in rare cases, locks) must be acquired to execute these sequences. Typically it is the lock on the instance itself, but there are exceptions. For example, the documentation for Collections.synchronizedMap says this:

在文档中记录一个有条件的线程安全类需要小心。你必须指出哪些调用序列需要外部同步,以及执行这些序列必须获得哪些锁(在极少数情况下是锁)。通常是实例本身的锁,但也有例外。例如,Collections.synchronizedMap 的文档提到:

It is imperative that the user manually synchronize on the returned map when iterating over any of its collection views:

当用户遍历其集合视图时,必须手动同步返回的 Map:

  1. Map<K, V> m = Collections.synchronizedMap(new HashMap<>());
  2. Set<K> s = m.keySet(); // Needn't be in synchronized block
  3. ...
  4. synchronized(m) { // Synchronizing on m, not s!
  5. for (K key : s)
  6. key.f();
  7. }

Failure to follow this advice may result in non-deterministic behavior.

不遵循这个建议可能会导致不确定的行为。

The description of a class’s thread safety generally belongs in the class’s doc comment, but methods with special thread safety properties should describe these properties in their own documentation comments. It is not necessary to document the immutability of enum types. Unless it is obvious from the return type, static factories must document the thread safety of the returned object, as demonstrated by Collections.synchronizedMap (above).

类的线程安全的描述通常属于该类的文档注释,但是具有特殊线程安全属性的方法应该在它们自己的文档注释中描述这些属性。没有必要记录枚举类型的不变性。除非从返回类型可以明显看出,否则静态工厂必须记录返回对象的线程安全性,正如 Collections.synchronizedMap 所演示的那样。

When a class commits to using a publicly accessible lock, it enables clients to execute a sequence of method invocations atomically, but this flexibility comes at a price. It is incompatible with high-performance internal concurrency control, of the sort used by concurrent collections such as ConcurrentHashMap. Also, a client can mount a denial-of-service attack by holding the publicly accessible lock for a prolonged period. This can be done accidentally or intentionally.

当一个类使用公共可访问锁时,它允许客户端自动执行一系列方法调用,但是这种灵活性是有代价的。它与诸如 ConcurrentHashMap 之类的并发集合所使用的高性能内部并发控制不兼容。此外,客户端可以通过长时间持有可公开访问的锁来发起拒绝服务攻击。这可以是无意的,也可以是有意的。

To prevent this denial-of-service attack, you can use a private lock object instead of using synchronized methods (which imply a publicly accessible lock):

为了防止这种拒绝服务攻击,你可以使用一个私有锁对象,而不是使用同步方法(隐含一个公共可访问的锁):

  1. // Private lock object idiom - thwarts denial-of-service attack
  2. private final Object lock = new Object();
  3. public void foo() {
  4. synchronized(lock) {
  5. ...
  6. }
  7. }

Because the private lock object is inaccessible outside the class, it is impossible for clients to interfere with the object’s synchronization. In effect, we are applying the advice of Item 15 by encapsulating the lock object in the object it synchronizes.

因为私有锁对象在类之外是不可访问的,所以客户端不可能干扰对象的同步。实际上,我们通过将锁对象封装在它同步的对象中,是在应用 Item-15 的建议。

Note that the lock field is declared final. This prevents you from inadvertently changing its contents, which could result in catastrophic unsynchronized access (Item 78). We are applying the advice of Item 17, by minimizing the mutability of the lock field. Lock fields should always be declared final. This is true whether you use an ordinary monitor lock (as shown above) or a lock from the java.util.concurrent.locks package.

注意,lock 字段被声明为 final。这可以防止你无意中更改它的内容,这可能导致灾难性的非同步访问(Item-78)。我们正在应用 Item-17 的建议,最小化锁字段的可变性。Lock 字段应该始终声明为 final。 无论使用普通的监视器锁(如上所示)还是 java.util.concurrent 包中的锁,都是这样。

The private lock object idiom can be used only on unconditionally thread-safe classes. Conditionally thread-safe classes can’t use this idiom because they must document which lock their clients are to acquire when performing certain method invocation sequences.

私有锁对象用法只能在无条件的线程安全类上使用。有条件的线程安全类不能使用这种用法,因为它们必须在文档中记录,在执行某些方法调用序列时要获取哪些锁。

The private lock object idiom is particularly well-suited to classes designed for inheritance (Item 19). If such a class were to use its instances for locking, a subclass could easily and unintentionally interfere with the operation of the base class, or vice versa. By using the same lock for different purposes, the subclass and the base class could end up “stepping on each other’s toes.” This is not just a theoretical problem; it happened with the Thread class [Bloch05, Puzzle 77].

私有锁对象用法特别适合为继承而设计的类(Item-19)。如果这样一个类要使用它的实例进行锁定,那么子类很容易在无意中干扰基类的操作,反之亦然。通过为不同的目的使用相同的锁,子类和基类最终可能「踩到对方的脚趾头」。这不仅仅是一个理论问题,它就发生在 Thread 类中 [Bloch05, Puzzle 77]。

To summarize, every class should clearly document its thread safety properties with a carefully worded prose description or a thread safety annotation. The synchronized modifier plays no part in this documentation. Conditionally thread-safe classes must document which method invocation sequences require external synchronization and which lock to acquire when executing these sequences. If you write an unconditionally thread-safe class, consider using a private lock object in place of synchronized methods. This protects you against synchronization interference by clients and subclasses and gives you more flexibility to adopt a sophisticated approach to concurrency control in a later release.

总之,每个类都应该措辞严谨的描述或使用线程安全注解清楚地记录其线程安全属性。synchronized 修饰符在文档中没有任何作用。有条件的线程安全类必须记录哪些方法调用序列需要外部同步,以及在执行这些序列时需要获取哪些锁。如果你编写一个无条件线程安全的类,请考虑使用一个私有锁对象来代替同步方法。这将保护你免受客户端和子类的同步干扰,并为你提供更大的灵活性,以便在后续的版本中采用复杂的并发控制方式。


【83】明智地使用延迟初始化

Use lazy initialization judiciously

Lazy initialization is the act of delaying the initialization of a field until its value is needed. If the value is never needed, the field is never initialized. This technique is applicable to both static and instance fields. While lazy initialization is primarily an optimization, it can also be used to break harmful circularities in class and instance initialization [Bloch05, Puzzle 51].

延迟初始化是延迟字段的初始化,直到需要它的值。如果不需要该值,则不会初始化字段。这种技术既适用于静态字段,也适用于实例字段。虽然延迟初始化主要是一种优化,it can also be used to break harmful circularities in class and instance initialization [Bloch05, Puzzle 51]。

As is the case for most optimizations, the best advice for lazy initialization is “don’t do it unless you need to” (Item 67). Lazy initialization is a double-edged sword. It decreases the cost of initializing a class or creating an instance, at the expense of increasing the cost of accessing the lazily initialized field. Depending on what fraction of these fields eventually require initialization, how expensive it is to initialize them, and how often each one is accessed once initialized, lazy initialization can (like many “optimizations”) actually harm performance.

与大多数优化一样,延迟初始化的最佳建议是「除非需要,否则不要这样做」(第 67 项)。延迟初始化是一把双刃剑。它降低了初始化类或创建实例的成本,代价是增加了访问延迟初始化字段的成本。根据这些字段中最终需要初始化的部分、初始化它们的开销以及初始化后访问每个字段的频率,延迟初始化实际上会损害性能(就像许多「优化」一样)。

That said, lazy initialization has its uses. If a field is accessed only on a fraction of the instances of a class and it is costly to initialize the field, then lazy initialization may be worthwhile. The only way to know for sure is to measure the performance of the class with and without lazy initialization.

延迟初始化也有它的用途。如果一个字段只在类的一小部分实例上访问,并且初始化该字段的代价很高,那么延迟初始化可能是值得的。唯一确定的方法是以使用和不使用延迟初始化的效果对比来度量类的性能。

In the presence of multiple threads, lazy initialization is tricky. If two or more threads share a lazily initialized field, it is critical that some form of synchronization be employed, or severe bugs can result (Item 78). All of the initialization techniques discussed in this item are thread-safe.

在存在多个线程的情况下,使用延迟初始化很棘手。如果两个或多个线程共享一个延迟初始化的字段,那么必须使用某种形式的同步,否则会导致严重的错误(Item-78)。本条目讨论的所有初始化技术都是线程安全的。

Under most circumstances, normal initialization is preferable to lazy initialization. Here is a typical declaration for a normally initialized instance field. Note the use of the final modifier (Item 17):

在大多数情况下,常规初始化优于延迟初始化。 下面是一个使用常规初始化的实例字段的典型声明。注意 final 修饰符的使用(Item-17):

  1. // Normal initialization of an instance field
  2. private final FieldType field = computeFieldValue();

If you use lazy initialization to break an initialization circularity, use a synchronized accessor because it is the simplest, clearest alternative:

如果您使用延迟初始化来取代初始化 circularity,请使用同步访问器,因为它是最简单、最清晰的替代方法:

  1. // Lazy initialization of instance field - synchronized accessor
  2. private FieldType field;
  3. private synchronized FieldType getField() {
  4. if (field == null)
  5. field = computeFieldValue();
  6. return field;
  7. }

Both of these idioms (normal initialization and lazy initialization with a synchronized accessor) are unchanged when applied to static fields, except that you add the static modifier to the field and accessor declarations.

这两种习惯用法(使用同步访问器进行常规初始化和延迟初始化)在应用于静态字段时都没有改变,只是在字段和访问器声明中添加了 static 修饰符。

If you need to use lazy initialization for performance on a static field, use the lazy initialization holder class idiom. This idiom exploits the guarantee that a class will not be initialized until it is used [JLS, 12.4.1]. Here’s how it looks:

如果需要在静态字段上使用延迟初始化来提高性能,use the lazy initialization holder class idiom. 这个用法可保证一个类在使用之前不会被初始化 [JLS, 12.4.1]。它是这样的:

  1. // Lazy initialization holder class idiom for static fields
  2. private static class FieldHolder {
  3. static final FieldType field = computeFieldValue();
  4. }
  5. private static FieldType getField() { return FieldHolder.field; }

When getField is invoked for the first time, it reads FieldHolder.field for the first time, causing the initialization of the FieldHolder class. The beauty of this idiom is that the getField method is not synchronized and performs only a field access, so lazy initialization adds practically nothing to the cost of access. A typical VM will synchronize field access only to initialize the class. Once the class is initialized, the VM patches the code so that subsequent access to the field does not involve any testing or synchronization.

第一次调用 getField 时,它执行 FieldHolder.field,导致初始化 FieldHolder 类。这个习惯用法的优点是 getField 方法不是同步的,只执行字段访问,所以延迟初始化实际上不会增加访问成本。典型的 VM 只会同步字段访问来初始化类。初始化类之后,VM 会对代码进行修补,这样对字段的后续访问就不会涉及任何测试或同步。

If you need to use lazy initialization for performance on an instance field, use the double-check idiom. This idiom avoids the cost of locking when accessing the field after initialization (Item 79). The idea behind the idiom is to check the value of the field twice (hence the name double-check): once without locking and then, if the field appears to be uninitialized, a second time with locking. Only if the second check indicates that the field is uninitialized does the call initialize the field. Because there is no locking once the field is initialized, it is critical that the field be declared volatile (Item 78). Here is the idiom:

如果需要使用延迟初始化来提高实例字段的性能,请使用双重检查模式。这个模式避免了初始化后访问字段时的锁定成本(Item-79)。这个模式背后的思想是两次检查字段的值(因此得名 double check):一次没有锁定,然后,如果字段没有初始化,第二次使用锁定。只有当第二次检查指示字段未初始化时,调用才初始化字段。由于初始化字段后没有锁定,因此将字段声明为 volatile 非常重要(Item-78)。下面是这个模式的示例:

  1. // Double-check idiom for lazy initialization of instance fields
  2. private volatile FieldType field;
  3. private FieldType getField() {
  4. FieldType result = field;
  5. if (result == null) { // First check (no locking)
  6. synchronized(this) {
  7. if (field == null) // Second check (with locking)
  8. field = result = computeFieldValue();
  9. }
  10. }
  11. return result;
  12. }

This code may appear a bit convoluted. In particular, the need for the local variable (result) may be unclear. What this variable does is to ensure that field is read only once in the common case where it’s already initialized.

这段代码可能看起来有点复杂。特别是不清楚是否需要局部变量(result)。该变量的作用是确保 field 在已经初始化的情况下只读取一次。

While not strictly necessary, this may improve performance and is more elegant by the standards applied to low-level concurrent programming. On my machine, the method above is about 1.4 times as fast as the obvious version without a local variable. While you can apply the double-check idiom to static fields as well, there is no reason to do so: the lazy initialization holder class idiom is a better choice.

虽然不是严格必需的,但这可能会提高性能,而且与低级并发编程相比,这更优雅。在我的机器上,上述方法的速度大约是没有局部变量版本的 1.4 倍。虽然您也可以将双重检查模式应用于静态字段,但是没有理由这样做:the lazy initialization holder class idiom is a better choice.

Two variants of the double-check idiom bear noting. Occasionally, you may need to lazily initialize an instance field that can tolerate repeated initialization. If you find yourself in this situation, you can use a variant of the double-check idiom that dispenses with the second check. It is, not surprisingly, known as the single-check idiom. Here is how it looks. Note that field is still declared volatile:

双重检查模式的两个变体值得注意。有时候,您可能需要延迟初始化一个实例字段,该字段可以容忍重复初始化。如果您发现自己处于这种情况,您可以使用双重检查模式的变体来避免第二个检查。毫无疑问,这就是所谓的「单检查」模式。它是这样的。注意,field 仍然声明为 volatile:

  1. // Single-check idiom - can cause repeated initialization!
  2. private volatile FieldType field;
  3. private FieldType getField() {
  4. FieldType result = field;
  5. if (result == null)
  6. field = result = computeFieldValue();
  7. return result;
  8. }

All of the initialization techniques discussed in this item apply to primitive fields as well as object reference fields. When the double-check or single-check idiom is applied to a numerical primitive field, the field’s value is checked against 0 (the default value for numerical primitive variables) rather than null.

本条目中讨论的所有初始化技术都适用于基本字段和对象引用字段。当双检查或单检查模式应用于数值基本类型字段时,将根据 0(数值基本类型变量的默认值)而不是 null 检查字段的值。

If you don’t care whether every thread recalculates the value of a field, and the type of the field is a primitive other than long or double, then you may choose to remove the volatile modifier from the field declaration in the single-check idiom. This variant is known as the racy single-check idiom. It speeds up field access on some architectures, at the expense of additional initializations (up to one per thread that accesses the field). This is definitely an exotic technique, not for everyday use.

如果您不关心每个线程是否都会重新计算字段的值,并且字段的类型是 long 或 double 之外的基本类型,那么您可以选择在单检查模式中从字段声明中删除 volatile 修饰符。这种变体称为原生单检查模式。它加快了某些架构上的字段访问速度,代价是需要额外的初始化(每个访问该字段的线程最多需要一个初始化)。这绝对是一种奇特的技术,不是日常使用的。

In summary, you should initialize most fields normally, not lazily. If you must initialize a field lazily in order to achieve your performance goals or to break a harmful initialization circularity, then use the appropriate lazy initialization technique. For instance fields, it is the double-check idiom; for static fields, the lazy initialization holder class idiom. For instance fields that can tolerate repeated initialization, you may also consider the single-check idiom.

总之,您应该正常初始化大多数字段,而不是延迟初始化。如果必须延迟初始化字段以实现性能目标或 break a harmful initialization circularity,则使用适当的延迟初始化技术。对于字段,使用双重检查模式;对于静态字段,the lazy initialization holder class idiom. 例如,可以容忍重复初始化的实例字段,您还可以考虑单检查模式。


【84】不要依赖线程调度器

Don’t depend on the thread scheduler

When many threads are runnable, the thread scheduler determines which ones get to run and for how long. Any reasonable operating system will try to make this determination fairly, but the policy can vary. Therefore, well-written programs shouldn’t depend on the details of this policy. Any program that relies on the thread scheduler for correctness or performance is likely to be nonportable.

当许多线程可以运行时,线程调度器决定哪些线程可以运行以及运行多长时间。任何合理的操作系统都会尝试公平地做出这个决定,但是策略可能会有所不同。因此,编写良好的程序不应该依赖于此策略的细节。任何依赖线程调度器来保证正确性或性能的程序都可能是不可移植的。

The best way to write a robust, responsive, portable program is to ensure that the average number of runnable threads is not significantly greater than the number of processors. This leaves the thread scheduler with little choice: it simply runs the runnable threads till they’re no longer runnable. The program’s behavior doesn’t vary too much, even under radically different thread-scheduling policies. Note that the number of runnable threads isn’t the same as the total number of threads, which can be much higher. Threads that are waiting are not runnable.

编写健壮、响应快、可移植程序的最佳方法是确保可运行线程的平均数量不显著大于处理器的数量。这使得线程调度器几乎没有选择:它只运行可运行线程,直到它们不再可运行为止。即使在完全不同的线程调度策略下,程序的行为也没有太大的变化。注意,可运行线程的数量与线程总数不相同,后者可能更高。正在等待的线程不可运行。

The main technique for keeping the number of runnable threads low is to have each thread do some useful work, and then wait for more. Threads should not run if they aren’t doing useful work. In terms of the Executor Framework (Item 80), this means sizing thread pools appropriately [Goetz06, 8.2] and keeping tasks short, but not too short, or dispatching overhead will harm performance.

保持可运行线程数量低的主要技术是让每个线程做一些有用的工作,然后等待更多的工作。如果线程没有做有用的工作,它们就不应该运行。 对于 Executor 框架(Item-80),这意味着适当调整线程池的大小 [Goetz06, 8.2],并保持任务短小(但不要太短),否则分派开销依然会损害性能。

Threads should not busy-wait, repeatedly checking a shared object waiting for its state to change. Besides making the program vulnerable to the vagaries of the thread scheduler, busy-waiting greatly increases the load on the processor, reducing the amount of useful work that others can accomplish. As an extreme example of what not to do, consider this perverse reimplementation of CountDownLatch:

线程不应该处于 busy 到 wait 的循环,而应该反复检查一个共享对象,等待它的状态发生变化。除了使程序容易受到线程调度器变化无常的影响之外,繁忙等待还大大增加了处理器的负载,还影响其他人完成工作。作为反面的极端例子,考虑一下 CountDownLatch 的不正确的重构实现:

  1. // Awful CountDownLatch implementation - busy-waits incessantly!
  2. public class SlowCountDownLatch {
  3. private int count;
  4. public SlowCountDownLatch(int count) {
  5. if (count < 0)
  6. throw new IllegalArgumentException(count + " < 0");
  7. this.count = count;
  8. }
  9. public void await() {
  10. while (true) {
  11. synchronized(this) {
  12. if (count == 0)
  13. return;
  14. }
  15. }
  16. }
  17. public synchronized void countDown() {
  18. if (count != 0)
  19. count--;
  20. }
  21. }

On my machine, SlowCountDownLatch is about ten times slower than Java’s CountDownLatch when 1,000 threads wait on a latch. While this example may seem a bit far-fetched, it’s not uncommon to see systems with one or more threads that are unnecessarily runnable. Performance and portability are likely to suffer.

在我的机器上,当 1000 个线程等待一个锁存器时,SlowCountDownLatch 的速度大约是 Java 的 CountDownLatch 的 10 倍。虽然这个例子看起来有点牵强,但是具有一个或多个不必要运行的线程的系统并不少见。性能和可移植性可能会受到影响。

When faced with a program that barely works because some threads aren’t getting enough CPU time relative to others, resist the temptation to “fix” the program by putting in calls to Thread.yield. You may succeed in getting the program to work after a fashion, but it will not be portable. The same yield invocations that improve performance on one JVM implementation might make it worse on a second and have no effect on a third. Thread.yield has no testable semantics. A better course of action is to restructure the application to reduce the number of concurrently runnable threads.

当面对一个几乎不能工作的程序时,而原因是由于某些线程相对于其他线程没有获得足够的 CPU 时间,那么 通过调用 Thread.yield 来「修复」程序 你也许能勉强让程序运行起来,但它是不可移植的。在一个 JVM 实现上提高性能的相同的 yield 调用,在第二个 JVM 实现上可能会使性能变差,而在第三个 JVM 实现上可能没有任何影响。Thread.yield 没有可测试的语义。 更好的做法是重构应用程序,以减少并发运行线程的数量。

A related technique, to which similar caveats apply, is adjusting thread priorities. Thread priorities are among the least portable features of Java. It is not unreasonable to tune the responsiveness of an application by tweaking a few thread priorities, but it is rarely necessary and is not portable. It is unreasonable to attempt to solve a serious liveness problem by adjusting thread priorities. The problem is likely to return until you find and fix the underlying cause.

一个相关的技术是调整线程优先级,类似的警告也适用于此技术,即,线程优先级是 Java 中最不可移植的特性之一。通过调整线程优先级来调优应用程序的响应性并非不合理,但很少情况下是必要的,而且不可移植。试图通过调整线程优先级来解决严重的活性问题是不合理的。在找到并修复潜在原因之前,问题很可能会再次出现。

In summary, do not depend on the thread scheduler for the correctness of your program. The resulting program will be neither robust nor portable. As a corollary, do not rely on Thread.yield or thread priorities. These facilities are merely hints to the scheduler. Thread priorities may be used sparingly to improve the quality of service of an already working program, but they should never be used to “fix” a program that barely works.

总之,不要依赖线程调度器来判断程序的正确性。生成的程序既不健壮也不可移植。因此,不要依赖 Thread.yield 或线程优先级。这些工具只是对调度器的提示。线程优先级可以少量地用于提高已经工作的程序的服务质量,但绝不应该用于「修复」几乎不能工作的程序。