Synchronization

Threads communicate primarily by sharing access to fields and the objects reference fields refer to. This form of communication is extremely efficient, but

makes two kinds of errors possible: thread interference and memory consistency errors. The tool needed to prevent these errors is synchronization.However, synchronization can introduce thread contention, which occurs when two or more threads try to access the same resource simultaneously and cause

the Java runtime to execute one or more threads more slowly, or even suspend their execution. Starvation and livelock are forms of thread contention. See the section Liveness for more information.This section covers the following topics:

Thread Interference describes how errors are introduced when multiple threads access shared data.
Memory Consistency Errors describes errors that result from inconsistent views of shared memory.
Synchronized Methods describes a simple idiom that can effectively prevent thread interference and memory consistency errors.
Implicit Locks and Synchronization describes a more general synchronization idiom, and describes how synchronization is based on implicit locks.
Atomic Access talks about the general idea of operations that can't be interfered with by other threads.

同步: 线程间通信主要通过共享字段权限以及字段所指向的对象引用。这种形式的通信非常有效率,但是但来了两个可能的错误:线程干扰和内存一致性错误。纠正这些错误的手段就是:同步。 但是,同步带了了线程间的资源的争夺,尤其是当多个线程师试图同时获取同一个资源时。这照成了java运行时环境执行程序的缓慢,甚至挂起执行程序。 Starvation and livelock 是两种线程争夺的形式。参考Liveness章节。 该部分包括: 线程干扰:当多个线程获取共享数据时错误是如何产生的。 内存一致性错误:描述了内存的不一致带来的错误。 同步方法:一个简单的处理上述错误的手段。 隐函锁和同步:描述了更一般的同步方法。描述了如何基于隐函锁实现的同步。 原子操作:讨论了不可被其他线程打扰的一般操作方法。

Thread Interference

Consider a simple class called Counter

class Counter { private int c = 0;

public void increment() {
    c++;
}

public void decrement() {
    c--;
}

public int value() {
    return c;
}

}

Counter is designed so that each invocation of increment will add 1 to c, and each invocation of decrement will subtract 1 from c. However, if a Counter object is referenced from multiple threads, interference between threads may prevent this from happening as expected.Interference happens when two operations, running in different threads, but acting on the same data, interleave. This means that the two operations consist of multiple steps, and the sequences of steps overlap. It might not seem possible for operations on instances of Counter to interleave, since both operations on c are single, simple statements. However, even simple statements can translate to multiple steps by the virtual machine. We won't examine the specific steps the virtual machine takes — it is enough to know that the single expression c++ can be decomposed into three steps:

Retrieve the current value of c.
Increment the retrieved value by 1.
Store the incremented value back in c.

The expression c-- can be decomposed the same way, except that the second step decrements instead of increments.

Suppose Thread A invokes increment at about the same time Thread B invokes decrement. If the initial value of c is 0, their interleaved actions might follow this

sequence:

Thread A: Retrieve c.
Thread B: Retrieve c.
Thread A: Increment retrieved value; result is 1.
Thread B: Decrement retrieved value; result is -1.
Thread A: Store result in c; c is now 1.
Thread B: Store result in c; c is now -1.

Thread A's result is lost, overwritten by Thread B. This particular interleaving is only one possibility. Under different circumstances it might be Thread B's result

that gets lost, or there could be no error at all. Because they are unpredictable, thread interference bugs can be difficult to detect and fix.

线程干扰 有如下类:counter。increment 将c加1.decrement 将c减1.如果一个conuter对象呗多个线程引用,线程间的干扰会打乱我们的期望结果。线程干扰发生在两个不同线程交错操作同一个数据时。这意味着这两个操作包好几个步骤,每个步骤将overlap之前的操作。 如果对c的操作是单个的,不会产生线程干扰。但是即使简单的声明也会被JVM翻译成好几步。我们不会检查具体的虚拟机的步骤,但是只要明白c++操作可以分成3个步骤。 1.取得c当前的值。

2. 将当前的值加1.
3.将2的结果返回给c

同样c--也可以分成上述步骤(第二部为间1)。 假设A线程调用increment方法,同时B线程调用decrement方法。如果c的初值为0,他们的干扰顺序或许如下: 1。A:取得c值 2。B:取得c值 3。A:将c值加1,结果是1. 4。B:将取得的c值减1,结果是-1. 5。A:保存c值,结果是1. 6。B:保存c值,结果是-1. 线程A的结果被线程B覆盖了。这只是其中的一直可能。不同的环境下结果不同,可能B的结果丢失了,也可能结果并没有错误。因为这是不可以预知的,所以线程干扰的bug很慢发现并解决。 Memory Consistency Errors

Memory consistency errors occur when different threads have inconsistent views of what should be the same data. The causes of memory consistency errors

are complex and beyond the scope of this tutorial. Fortunately, the programmer does not need a detailed understanding of these causes. All that is needed is a

strategy for avoiding them.The key to avoiding memory consistency errors is understanding the happens-before relationship. This relationship is simply a guarantee that memory writes by one specific statement are visible to another specific statement. To see this, consider the following example. Suppose a simple int field is defined and initialized:

int counter = 0;

The counter field is shared between two threads, A and B. Suppose thread A increments counter:

counter++;

Then, shortly afterwards, thread B prints out counter:

System.out.println(counter);

If the two statements had been executed in the same thread, it would be safe to assume that the value printed out would be "1". But if the two statements are executed in separate threads, the value printed out might well be "0", because there's no guarantee that thread A's change to counter will be visible to thread B — unless the programmer has established a happens-before relationship between these two statements.

There are several actions that create happens-before relationships. One of them is synchronization, as we will see in the following sections.

We've already seen two actions that create happens-before relationships.

When a statement invokes Thread.start, every statement that has a happens-before relationship with that statement also has a happens-before relationship with 

every statement executed by the new thread. The effects of the code that led up to the creation of the new thread are visible to the new thread. When a thread terminates and causes a Thread.join in another thread to return, then all the statements executed by the terminated thread have a happens-

before relationship with all the statements following the successful join. The effects of the code in the thread are now visible to the thread that performed the

join.For a list of actions that create happens-before relationships, refer to the Summary page of the java.util.concurrent package..

内存一致性错误 当不同的线程针对同一个数据有不同的值得时候,内存一致性错误就发生了。错误的原因很复杂,超出了本教程的范围。幸运的是,程序员不需要详细了解这些原因,只需要知道如何避免即可。 避免内存一致性错误的关键是理解happens-before relationship。这个关系仅仅保证被一个语句写的内存对 其他特定的语句可见。

参考如下例子:有一个int类型的field,初始化为0;int conuter =0; conuter字段被A和B两个线程共享。如果A将counter加1:couter++;不久后B线程打印counter。 如果这两个语句在同一个线程执行,我们可以肯定打印的结果是1.但是如果在两个不同的线程执行,打印结果可能是0,因为没有保证A对counter的改变对B可见。除非程序员在这两个语句之间建立了happens-before relationship。 有多重方法可以创建happens-before relationship。其中之一是同步。我们目前已知的两个途径是: 1.当一个语句唤醒Thread.start,每一个与该语句有happens-before relationship的语句和新线程里的语句同样有happens-before

relationship。创建新线程的代码结果对新线程可见。 2.当一个线程结束,并且使得等待该线程的Thread.join语句返回到等待线程。那么,结束的线程的所有的语句对成功执行的join语句都有一个happens-before relationship。代码的与运行结果对join语句所在线程有happens-before relationship。 happens-before relationship可以参看Summary page of the java.util.concurrent package..。 Synchronized Methods

The Java programming language provides two basic synchronization idioms: synchronized methods and synchronized statements. The more complex of the

two, synchronized statements, are described in the next section. This section is about synchronized methods.

To make a method synchronized, simply add the synchronized keyword to its declaration:

public class SynchronizedCounter { private int c = 0;

public synchronized void increment() {
    c++;
}

public synchronized void decrement() {
    c--;
}

public synchronized int value() {
    return c;
}

}

If count is an instance of SynchronizedCounter, then making these methods synchronized has two effects:

First, it is not possible for two invocations of synchronized methods on the same object to interleave. When one thread is executing a synchronized method 

for an object, all other threads that invoke synchronized methods for the same object block (suspend execution) until the first thread is done with the object. Second, when a synchronized method exits, it automatically establishes a happens-before relationship with any subsequent invocation of a synchronized

method for the same object. This guarantees that changes to the state of the object are visible to all threads.

Note that constructors cannot be synchronized — using the synchronized keyword with a constructor is a syntax error. Synchronizing constructors doesn't

make sense, because only the thread that creates an object should have access to it while it is being constructed. Warning: When constructing an object that will be shared between threads, be very careful that a reference to the object does not "leak" prematurely. For

example, suppose you want to maintain a List called instances containing every instance of class. You might be tempted to add the following line to your

constructor:

instances.add(this);

But then other threads can use instances to access the object before construction of the object is complete.

Synchronized methods enable a simple strategy for preventing thread interference and memory consistency errors: if an object is visible to more than one

thread, all reads or writes to that object's variables are done through synchronized methods. (An important exception: final fields, which cannot be modified

after the object is constructed, can be safely read through non-synchronized methods, once the object is constructed) This strategy is effective, but can present

problems with liveness, as we'll see later in this lesson.

java语言提供了两个基本的同步方法:同步方法和同步语句。同步语句更加复杂。 通过在方法声明前添加synchronized关键字,完成同步方法的声明。如果count是SynchronizedCounter的实例,那么这些同步方法有两个作用: 1.同步方法的两个调用不可能交错。当一个线程在一个对象中执行同步方法时,所有的其他线程在电泳该同步方法时会堵塞。(挂起执行)直到第一个线程完成该对象的处理。 2.当一个同步线程退出,它就会自动与后续的调用该对象的同步方法的语句建立一个happens-before relationship。这就保证了对一个对象的改变对其他线程可见。 注意:构造函数不可以被synchronized。否则会照成语法错误。而且同步构造方法没有意义,因为,创建对象的线程只有在对象被construct之后才可以对他进行操作。警告:当构造一个可以被多个线程共享的对象时,要小心该对象的引用不要过早被泄露。有一个包含了每一个实例的List:instances。如果你构造函数后有如下语句instances.add(this);
但是其他的线程可以在对象构造完成之前使用instances来获取对象。 同步方法为阻止线程干扰和内存一致性错误提供了简单的策略:如果哦一个对象对其他线程可见,所有对该对象的读和写都通过synchrosed方法(final 字段在构造之后不可以被改变,因此可以不通过同步方法对齐进行读操作)。该策略有效,但是带来了liveness问题。

Intrinsic Locks and Synchronization

Synchronization is built around an internal entity known as the intrinsic lock or monitor lock. (The API specification often refers to this entity simply as a "monitor.") Intrinsic locks play a role in both aspects of synchronization: enforcing exclusive access to an object's state and establishing happens-before relationships that are essential to visibility.

Every object has an intrinsic lock associated with it. By convention, a thread that needs exclusive and consistent access to an object's fields has to acquire the object's intrinsic lock before accessing them, and then release the intrinsic lock when it's done with them. A thread is said to own the intrinsic lock between the time it has acquired the lock and released the lock. As long as a thread owns an intrinsic lock, no other thread can acquire the same lock. The other thread will block when it attempts to acquire the lock.

When a thread releases an intrinsic lock, a happens-before relationship is established between that action and any subsequent acquisition of the same lock. Locks In Synchronized Methods

When a thread invokes a synchronized method, it automatically acquires the intrinsic lock for that method's object and releases it when the method returns. The lock release occurs even if the return was caused by an uncaught exception.

You might wonder what happens when a static synchronized method is invoked, since a static method is associated with a class, not an object. In this case, the thread acquires the intrinsic lock for the Class object associated with the class. Thus access to class's static fields is controlled by a lock that's distinct from the lock for any instance of the class. Synchronized Statements

Another way to create synchronized code is with synchronized statements. Unlike synchronized methods, synchronized statements must specify the object that provides the intrinsic lock:

public void addName(String name) { synchronized(this) { lastName = name; nameCount++; } nameList.add(name); }

In this example, the addName method needs to synchronize changes to lastName and nameCount, but also needs to avoid synchronizing invocations of other objects' methods. (Invoking other objects' methods from synchronized code can create problems that are described in the section on Liveness.) Without synchronized statements, there would have to be a separate, unsynchronized method for the sole purpose of invoking nameList.add.

Synchronized statements are also useful for improving concurrency with fine-grained synchronization. Suppose, for example, class MsLunch has two instance fields, c1 and c2, that are never used together. All updates of these fields must be synchronized, but there's no reason to prevent an update of c1 from being interleaved with an update of c2 — and doing so reduces concurrency by creating unnecessary blocking. Instead of using synchronized methods or otherwise using the lock associated with this, we create two objects solely to provide locks.

public class MsLunch { private long c1 = 0; private long c2 = 0; private Object lock1 = new Object(); private Object lock2 = new Object();

public void inc1() {
    synchronized(lock1) {
        c1++;
    }
}

public void inc2() {
    synchronized(lock2) {
        c2++;
    }
}

}

Use this idiom with extreme care. You must be absolutely sure that it really is safe to interleave access of the affected fields. Reentrant Synchronization

Recall that a thread cannot acquire a lock owned by another thread. But a thread can acquire a lock that it already owns. Allowing a thread to acquire the same lock more than once enables reentrant synchronization. This describes a situation where synchronized code, directly or indirectly, invokes a method that also contains synchronized code, and both sets of code use the same lock. Without reentrant synchronization, synchronized code would have to take many additional precautions to avoid having a thread cause itself to block.

内部锁和同步 同步机制是通过内部实体:内部锁和监控锁实现的。(API说明中经常称之为监控器)。内部锁有两个作用:同步,排他性的操作一个对象的状态;建立 happens-before relationships。 每一个对象都有一个对应的内部锁。一般来说,如果一个线程需要排他性的一直操作一个对象的字段,那么他必须在操作之前获得该对象的内部锁。当操作完毕,释放内部锁。在获得内部锁之后释放内部锁之前,线程拥有该内部锁。一旦一个线程拥有可内部锁,没有其他的线程可以获得该锁。其他的线程在获取该内部锁时活阻塞。 当一个线程释放了内部锁,该动作和以后的获取同样锁的后续动作之间就简历了 happens-before relationship。 同步方法的锁 当一个线程唤醒一个同步方法,就会自动获取该方法的对象的内部锁,并在方法返回时释放该锁。即使return语句是被一个没有被捕获的语句引起,也会释放内部锁。 static方法与class紧密相连而不是对象,因此,线程获取与类相关联的Class(大class)对象的内部锁。因此,类的静态字段的锁与类的任何实例的锁不同。 同步声明 通过同步声明来创建同步代码。与同步方法不同,同步声明必须规定提供内部锁的对象。 public void addName(String name) { synchronized(this) { lastName = name; nameCount++; } nameList.add(name); } 这个例子中,addName方法需要同步的修改lastName和nameCount。但是也需要避免同步调用其他对象的方法。(在同步声明中调用其他对象的方法或照成Liveness问题)。 同步声明也可以通过fine-gained同步提高并发。假如:MsLunch类有两个字段,c1和c2,这两个字段绝不会一起使用。所有对这些字段的更新必须同步。但是没有理由阻止对c1的更新会与c2的更新交错。而且,一旦交错之后会产生阻塞并降低并发。相反,不使用同步方法或者this的锁,我们可以创建两个单独的对象来提供锁。

public class MsLunch {
    private long c1 = 0;
    private long c2 = 0;
    private Object lock1 = new Object();
    private Object lock2 = new Object();

    public void inc1() {
        synchronized(lock1) {
            c1++;
        }
    }

    public void inc2() {
        synchronized(lock2) {
            c2++;
        }
    }
}

使用这种方法必须确定交错的操作这些字段是安全的。 Reentrant Synchronization 重新调用一个线程不会获得其他线程拥有的资源。但是,一个线程可以获得他已经拥有的锁。允许一个线程获得同样的锁超过一次就是reentrant synchronization:当一个同步代码块直接或者间接调用了另一个同步代码块,而且两个代码块使用同样的锁。如果没有reentrant synchronization,同步代码块必须使用额外的措施避免一个线程自己阻塞自己。 Atomic Access

In programming, an atomic action is one that effectively happens all at once. An atomic action cannot stop in the middle: it either happens completely, or it doesn't happen at all. No side effects of an atomic action are visible until the action is complete.

We have already seen that an increment expression, such as c++, does not describe an atomic action. Even very simple expressions can define complex actions that can decompose into other actions. However, there are actions you can specify that are atomic:

Reads and writes are atomic for reference variables and for most primitive variables (all types except long and double).
Reads and writes are atomic for all variables declared volatile (including long and double variables).

Atomic actions cannot be interleaved, so they can be used without fear of thread interference. However, this does not eliminate all need to synchronize atomic actions, because memory consistency errors are still possible. Using volatile variables reduces the risk of memory consistency errors, because any write to a volatile variable establishes a happens-before relationship with subsequent reads of that same variable. This means that changes to a volatile variable are always visible to other threads. What's more, it also means that when a thread reads a volatile variable, it sees not just the latest change to the volatile, but also the side effects of the code that led up the change.

Using simple atomic variable access is more efficient than accessing these variables through synchronized code, but requires more care by the programmer to avoid memory consistency errors. Whether the extra effort is worthwhile depends on the size and complexity of the application.

Some of the classes in the java.util.concurrent package provide atomic methods that do not rely on synchronization. We'll discuss them in the section on High Level Concurrency Objects.

原子操作: 在程序中,一个院子操作意味着一次完成所有的地址。一个原子操作不可以在中间停下来,它要么不执行,要发生就执行完毕。在原子操作结束事前没有side effect可见。 我们 已经看打了自加操作c++并不是一个原子操作。即使非常简单的表达式也包含非常复杂的操作,并可以分解成几个部分。但是,有些操作是原子的: 1.除了long和double类型的数据,读写基本数据类型的操作是原子操作。 2.所偶有volatile类型的变量的读写操作都是原子操作。 原子操作不可以与其他线程交叠,所以,使用它们可以避免线程干扰。但是,因为无法避免内存一致性错误,我们还不可避免使用同步原子操作。使用volatile变量,减少了内存一致性错误,因为任何一个volatile变量的读写操作都会与后面的对该变量的读操作建立 happens-before relationship。这意味着对一个volatile类型的变量的改变对其他线程都可见。另外,当一个线程读volatile变量的时候,他不仅可以看到最近的volatile变量的改变,还可以看到导致改变的代码的side effects。 使用原子操作比使用同步代码块更有效率。到那时需要非常小心内存错误。是否使用取决于应用的复杂度和代码量。

results matching ""

    No results matching ""