1.3. Risks of Threads(使用线程带来的风险)

1.3. Risks of Threads(使用线程带来的风险)
Java's built-in support for threads is a double-edged sword. While it simplifies the development of concurrent applications by providing language and library support and a formal cross-platform memory model (it is this formal cross-platform memory model that makes possible the development of write-once, run-anywhere concurrent applications in Java), it also raises the bar for developers because more programs will use threads. When threads were more esoteric, concurrency was an "advanced" topic; now, mainstream developers must be aware of thread-safety issues.
Java内在的支持线程本身是一把双刃剑。一方面,java为并发程序的开发通过提供语言和类库以及跨平台的内存模型(这里指的跨平台是指使用java可以开发“write-once, run-anywhere”的并发程序)。正基于此,java也为开发者设置了障碍-因为越来越多的程序开始使用线程。由于线程在其他语言中是非常神秘的,因此并发是一个高难度的课题,如果使用java,大多数的开发者都要注意线程安全的问题。
1.3.1. Safety Hazards(安全问题)
Thread safety can be unexpectedly subtle because, in the absence of sufficient synchronization, the ordering of operations in multiple threads is unpredictable and sometimes surprising. Unsafe Sequence in Listing 1.1, which is supposed to generate a sequence of unique integer values, offers a simple illustration of how the interleaving of actions in multiple threads can lead to undesirable results. It behaves correctly in a single-threaded environment, but in a multithreaded environment does not.
线程安全问题有时候可能很微妙的,因为缺乏恰当的同步机制,多个线程中的执行序列可能是难以预期的,比如在List1.1中,类UnsafeSequence被用来产生一个唯一的整数值,但是多线程在交互执行的时候,有时候会得到不希望得到的结果,这个类就是一个简单的例证。这个类在单线程的环境中可能会运行良好,但是在多线程环境中却不会这样。
Listing 1.1. Non-thread-safe Sequence Generator.

@NotThreadSafe
public class UnsafeSequence {
    private int value;

    /** Returns a unique value. */
    public int getNext() {
        return value++;
    }
}

The problem with UnsafeSequence is that with some unlucky timing, two threads could call getNext and receive the same value. Figure 1.1 shows how this can happen. The increment notation, nextValue++, may appear to be a single operation, but is in fact three separate operations: read the value, add one to it, and write out the new value. Since operations in multiple threads may be arbitrarily interleaved by the runtime, it is possible for two threads to read the value at the same time, both see the same value, and then both add one to it. The result is that the same sequence number is returned from multiple calls in different threads.
Figure 1.1. Unlucky Execution of UnsafeSequence.Nextvalue.

UnsafeSequence的问题在于会存在“霉运陷阱”,落入霉运陷阱的两个线程在调用getNext方法的时候有可能会得到同样的值。Figure 1.1展示了这种情况是如何发生的。nextValue++这条递增语句,表明上是一个单独的操作,实际上这个操作分成了三个单独的步骤,读取数值,加1,输出新的值。由于多线程中的操作在运行过程中有可能是任意顺序执行的,因此有可能会存在两个线程同时读取,这样这两个线程会看到同样的值,这样就出现了不同的线程在分别调用方法的时候得到同样的数值。

Diagrams like Figure 1.1 depict possible interleavings of operations in different threads. In these diagrams, time runs from left to right, and each line represents the activities of a different thread. These interleaving diagrams usually depict the worst case[2] and are intended to show the danger of incorrectly assuming things will happen in a particular order.

Figure 1.1中的视图展现了不同的线程间一种可能的交叉执行方式。在这类视图中,时间从左向右执行,每一行代表了不同线程上的活动。这些活动交互视图通常用来描绘最差情况,以求展现在一些特定时序下出现的不正确的执行序列所带来的威胁。

[2] Actually, as we'll see in Chapter 3, the worst case can be even worse than these diagrams usually show because of the possibility of reordering.
实际上,在第三章中,我们将会看到由于可能的执行序列所带来的更糟糕的情形。
UnsafeSequence uses a nonstandard annotation: @NotThreadSafe. This is one of several custom annotations used throughout this book to document concurrency properties of classes and class members. (Other class-level annotations used in this way are @ThreadSafe and @Immutable; see Appendix A for details.) Annotations documenting thread safety are useful to multiple audiences. If a class is annotated with @THReadSafe, users can use it with confidence in a multithreaded environment, maintainers are put on notice that it makes thread safety guarantees that must be preserved, and software analysis tools can identify possible coding errors.
UnsafeSequence使用了一个非标准的java注解。这是贯穿于本书中的的几个自定义的java注解之一(其他类级别的使用方法是@ThreadSafe 和 @Immutable,具体使用细节见 附录1)。在文档中存在的线程安全的标记对以下几种人员是有用的,对开发人员而言,他们可以放心的将该类用于多线程环境中。维护人员则必须要注意维持该类的线程安全性。代码检查工具可以识别出可能的错误代码。
UnsafeSequence illustrates a common concurrency hazard called a race condition. Whether or not nextValue returns a unique value when called from multiple threads, as required by its specification, depends on how the runtime interleaves the operations which is not a desirable state of affairs.
UnsafeSequence展现了一个常见的并发危害,这被称之为“race condition”,nextValue方法是否能够返回一个唯一的整数值(如果需求中描述的那样),取决于多线程在运行时如果交叉执行时序操作,而这些时序操作的状态通常是不可琢磨的。
Because threads share the same memory address space and run concurrently, they can access or modify variables that other threads might be using. This is a tremendous convenience, because it makes data sharing much easier than would other inter-thread communications mechanisms. But it is also a significant risk: threads can be confused by having data change unexpectedly. Allowing multiple threads to access and modify the same variables introduces an element of nonsequentiality into an otherwise sequential programming model, which can be confusing and difficult to reason about. For a multithreaded program's behavior to be predictable, access to shared variables must be properly coordinated so that threads do not interfere with one another. Fortunately, Java provides synchronization mechanisms to coordinate such access.
由于线程之间共享相同内存空间,因此在并发执行过程中可能会访问和修改其他线程正在使用的内存空间。这首先带来了一种极大的便利性,因为这使得数据的共享比起其他线程间共享机制更加容易。但同时这也意味的巨大的威胁,线程中的数据可能会被错误的改变。允许多线程去访问和更改相同的变量给顺序编程模型带来了一种非顺序的元素。为了使得多线程程序的行为是可以被预见的,访问共享变量的线程必须被恰当的规划已放置线程之间互相打扰。幸运的是,Java语言提供了同步机制来实现这样的目标。
UnsafeSequence can be fixed by making getNext a synchronized method, as shown in Sequence in Listing 1.2,[3] thus preventing the unfortunate interaction in Figure 1.1. (Exactly why this works is the subject of Chapters 2 and 3.)
通过将UnsafeSequence的getNext方法修改成同步方法可以实现这一目标,如果Listing 1.2所示。这样一来,就可以放置Listing 1.1中出现的“霉运陷阱”的出现(至于这样的修改时如何起作用的,我们将会在第二章和第三章中讨论)。
[3] @GuardedBy is described in Section 2.4; it documents the synchronization policy for Sequence.
@GuardedBy将会在Section 2.4描述,他表明了时序的同步策略。
Listing 1.2. Thread-safe Sequence Generator.
@ThreadSafe
public class Sequence {
    @GuardedBy("this") private int nextValue;

    public synchronized int getNext() {
        return nextValue++;
    }
}

In the absence of synchronization, the compiler, hardware, and runtime are allowed to take substantial liberties with the timing and ordering of actions, such as caching variables in registers or processor-local caches where they are temporarily (or even permanently) invisible to other threads. These tricks are in aid of better performance and are generally desirable, but they place a burden on the developer to clearly identify where data is being shared across threads so that these optimizations do not undermine safety. (Chapter 16 gives the gory details on exactly what ordering guarantees the JVM makes and how synchronization affects those guarantees, but if you follow the rules in Chapters 2 and 3, you can safely avoid these low-level details.)
在缺少同步机制的情况下,编译器、硬件和运行时环境在执行时间和动作时序上有非常大的随意性,例如可以在寄存器或者处理器的本地缓存中缓存变量,而这些变量对其他线程来说应该是临时不可见的(甚至是永久不可见的)。为了获得更好的执行效率,这样的小技巧通常是可取的。但是这些行为也同时要求开发者必须要清楚的知道数据被保存的地方,以便这种优化不会导致安全缺陷。(在第16章中将会给出JVM本身所能保证的执行顺序,以及synchronization关键字是如何影响这种执行顺序的,但是如果你遵循第二章、第三章中的要求,你就可以回避掉这些底层的细节,并能够写出安全的代码)
1.3.2. Liveness Hazards(存活性问题)
It is critically important to pay attention to thread safety issues when developing concurrent code: safety cannot be compromised. The importance of safety is not unique to multithreaded programs single-threaded programs also must take care to preserve safety and correctness but the use of threads introduces additional safety hazards not present in single-threaded programs. Similarly, the use of threads introduces additional forms of liveness failure that do not occur in single-threaded programs.
安全是第一要务,因此在开发并发程序的时候,必须要在线程安全方便予以足够的重视。虽然单线程程序也必须要注意保证安全性和正确性,但是多线程程序的确引入了单线程程序中不不存在的威胁安全的因素。同样,线程的使用引入了存活性的问题,这个问题同样不会出现在单线程程序中。
While safety means "nothing bad ever happens", liveness concerns the complementary goal that "something good eventually happens". A liveness failure occurs when an activity gets into a state such that it is permanently unable to make forward progress. One form of liveness failure that can occur in sequential programs is an inadvertent infinite loop, where the code that follows the loop never gets executed. The use of threads introduces additional liveness risks. For example, if thread A is waiting for a resource that thread B holds exclusively, and B never releases it, A will wait forever. Chapter 10 describes various forms of liveness failures and how to avoid them, including deadlock (Section 10.1), starvation (Section 10.3.1), and livelock (Section 10.3.3). Like most concurrency bugs, bugs that cause liveness failures can be elusive because they depend on the relative timing of events in different threads, and therefore do not always manifest themselves in development or testing.
如果说安全性意味着“不会发生不幸的事情”,存活性则关注于另外一个互补的目标:“某些好的事情终究会发生”。存活性问题是一个活动出现了这样一种状态:无论如何都不能继续执行。一种形式的存活性问题出现在由于粗心大意而出现无线循环,当代码进入这样的循环时,就再也无法继续执行了。线程的引入进一步加大了出现这种问题的可能性。例如,如果线程A正在被线程B独占的资源,而线程B永远不会释放该资源,这样A就会无限制的等待下去。第十章将会描绘存活性问题的各种形式,并且将会介绍如何来避免这种情况的发生。存活性问题的诱因通常包括:死锁,饿死、活锁。跟其他的并发bug一样,存活性问题引起的bug也是难以琢磨的因为这样的bug出现依赖于不同线程中时间出现的时序,因此在开发和测试过程中很难显露出来。
1.3.3. Performance Hazards(性能问题)
Related to liveness is performance. While liveness means that something good eventually happens, eventually may not be good enough we often want good things to happen quickly. Performance issues subsume a broad range of problems, including poor service time, responsiveness, throughput, resource consumption, or scalability. Just as with safety and liveness, multithreaded programs are subject to all the performance hazards of single-threaded programs, and to others as well that are introduced by the use of threads.
与存活性相关的是性能。虽然存活性能够保证正确的事情迟早会发生,但是最终会发生还是不够的,我们通常会要求正确的事情尽快的发生。性能的讨论涉及到一系列广泛的问题,包括不可接受的服务时间、响应性、吞吐率、资源消耗、可伸缩性等等。跟安全性和存活性一样,引起多线程程序中性能问题的因素有一些也存在于单线程程序中,但是有一些是由于线程的使用所产生的,这些事多线程程序所独有的。
In well designed concurrent applications the use of threads is a net performance gain, but threads nevertheless carry some degree of runtime overhead. Context switches when the scheduler suspends the active thread temporarily so another thread can run are more frequent in applications with many threads, and have significant costs: saving and restoring execution context, loss of locality, and CPU time spent scheduling threads instead of running them. When threads share data, they must use synchronization mechanisms that can inhibit compiler optimizations, flush or invalidate memory caches, and create synchronization traffic on the shared memory bus. All these factors introduce additional performance costs; Chapter 11 covers techniques for analyzing and reducing these costs.
在设计良好的并发应用程序中,线程的使用是为了获得性能的提供,但是线程本身会带来一定程度的运行时负荷。在拥有过多线程的应用程序中,将会出现线程间的频繁切换,这样做的目的是为了将一个活动线程暂停以便让别的线程有运行的机会。这些切换会带来客观的性能损耗,保存和恢复执行上下文,断点的保存,这样cpu时间被用于调度线程,而不是去执行他们。当线程共享数据的时候,必须要引入同步机制。而同步机制会导致编译器的优化机制失效,清空或者使内存缓存失效,并且会在共享内存主线上产生同步传输。所有的这些因素会引入额外的性能缺失。第十一章会讨论使用什么样子的技术来分析这些损耗,以及降低这些损耗的方式。

你可能感兴趣的:(多线程,thread,活动,Access,performance)