Java开发网 Java开发网
注册 | 登录 | 帮助 | 搜索 | 排行榜 | 发帖统计  

您没有登录

» Java开发网 » Java SE 综合讨论区  

按打印兼容模式打印这个话题 打印话题    把这个话题寄给朋友 寄给朋友    该主题的所有更新都将Email到你的邮箱 订阅主题
flat modethreaded modego to previous topicgo to next topicgo to back
作者 Re:如何对一个变量进行同步与互斥操作? [Re:RichardCheung]
dingligang





发贴: 28
积分: 0
于 2003-12-21 14:35 user profilesend a private message to usersearch all posts byselect and copy to clipboard. 
ie only, sorry for netscape users:-)add this post to my favorite list
转自:Concurrent Programming in Java(2th)

2.2 Synchronization
Locking protects against low-level storage conflicts and corresponding high-level invariant failures. For example, consider the following class:

class Even { // Do not use
private int n = 0;
public int next(){ // POST?: next is always even
++n;
++n;
return n;
}
}

Without locking, the desired postcondition may fail due to a storage conflict when two or more threads execute the next method of the same Even object. Here is one possible execution trace, showing only the reads and writes to variable n that would result from the putfields and getfields in compiled bytecode.

Thread A Thread B
read 0
write 1
read 1
write 2
read 2 read 2
write 3
write 3 return 3
return 3

As is typical in concurrent programs, most traces of two threads concurrently executing Even.next do not display this safety violation. Programs using this version of Even are likely to pass some tests but are almost sure to break eventually. Such safety violations can be rare and difficult to test for, yet can have devastating effects. This motivates the careful, conservative design practices seen in reliable concurrent programs.

Declaring the next method as synchronized would preclude such conflicting traces. Locking serializes the execution of synchronized methods. So here, either thread A's next method would execute in full before thread B's, or vice versa.

2.2.1 Mechanics
As a preliminary to further discussions of design strategies based on locking, here is a summary of mechanics, as well as some usage notes surrounding the synchronized keyword.

2.2.1.1 Objects and locks
Every instance of class Object and its subclasses possesses a lock. Scalars of type int, float, etc., are not Objects. Scalar fields can be locked only via their enclosing objects. Individual fields cannot be marked as synchronized. Locking may be applied only to the use of fields within methods. However, as described in § 2.2.7.4, fields can be declared as volatile, which affects atomicity, visibility, and ordering properties surrounding their use.

Similarly, array objects holding scalar elements possess locks, but their individual scalar elements do not. (Further, there is no way to declare array elements as volatile.) Locking an array of Objects does not automatically lock all its elements. There are no constructs for simultaneously locking multiple objects in a single atomic operation.

Class instances are Objects. As described below, the locks associated with Class objects are used in static synchronized methods.

2.2.1.2 Synchronized methods and blocks
There are two syntactic forms based on the synchronized keyword, blocks and methods. Block synchronization takes an argument of which object to lock. This allows any method to lock any object. The most common argument to synchronized blocks is this.

Block synchronization is considered more fundamental than method synchronization. A declaration:

synchronized void f() { /* body */ }

is equivalent to:

void f() { synchronized(this) { /* body */ } }

The synchronized keyword is not considered to be part of a method's signature. So the synchronized modifier is not automatically inherited when subclasses override superclass methods, and methods in interfaces cannot be declared as synchronized. Also, constructors cannot be qualified as synchronized (although block synchronization can be used within constructors).

Synchronized instance methods in subclasses employ the same lock as those in their superclasses. But synchronization in an inner class method is independent of its outer class. However, a non-static inner class method can lock its containing class, say OuterClass, via code blocks using:

synchronized(OuterClass.this) { /* body */ }.

2.2.1.3 Acquiring and releasing locks
Locking obeys a built-in acquire-release protocol controlled only by use of the synchronized keyword. All locking is block-structured. A lock is acquired on entry to a synchronized method or block, and released on exit, even if the exit occurs due to an exception. You cannot forget to release a lock.

Locks operate on a per-thread, not per-invocation basis. A thread hitting synchronized passes if the lock is free or the thread already possess the lock, and otherwise blocks. (This reentrant or recursive locking differs from the default policy used for example in POSIX threads.) Among other effects, this allows one synchronized method to make a self-call to another synchronized method on the same object without freezing up.

A synchronized method or block obeys the acquire-release protocol only with respect to other synchronized methods and blocks on the same target object. Methods that are not synchronized may still execute at any time, even if a synchronized method is in progress. In other words, synchronized is not equivalent to atomic, but synchronization can be used to achieve atomicity.

When one thread releases a lock, another thread may acquire it (perhaps the same thread, if it hits another synchronized method). But there is no guarantee about which of any blocked threads will acquire the lock next or when they will do so. (In particular, there are no fairness guarantees — see § 3.4.1.5.) There is no mechanism to discover whether a given lock is being held by some thread.

As discussed in § 2.2.7, in addition to controlling locking, synchronized also has the side-effect of synchronizing the underlying memory system.

2.2.1.4 Statics
Locking an object does not automatically protect access to static fields of that object's class or any of its superclasses. Access to static fields is instead protected via synchronized static methods and blocks. Static synchronization employs the lock possessed by the Class object associated with the class the static methods are declared in. The static lock for class C can also be accessed inside instance methods via:

synchronized(C.class) { /* body */ }

The static lock associated with each class is unrelated to that of any other class, including its superclasses. It is not effective to add a new static synchronized method in a subclass that attempts to protect static fields declared in a superclass. Use the explicit block version instead.

It is also poor practice to use constructions of the form:

synchronized(getClass()) { /* body */ } // Do not use

This locks the actual class, which might be different from (a subclass of) the class defining the static fields that need protecting.

The JVM internally obtains and releases the locks for Class objects during class loading and initialization. Unless you are writing a special ClassLoader or holding multiple locks during static initialization sequences, these internal mechanics cannot interfere with the use of ordinary methods and blocks synchronized on Class objects. No other internal JVM actions independently acquire any locks for any objects of classes that you create and use. However, if you subclass java.* classes, you should be aware of the locking policies used in these classes.

2.2.2 Fully Synchronized Objects

A lock is the most basic kind of message acceptance control mechanism. Locks may be used to block clients attempting to invoke a method on an object while another method or code block (running in a different thread) is in progress.

The safest (but not always the best) concurrent OO design strategy based on locking is to restrict attention to fully synchronized objects (also known as atomic objects) in which:

All methods are synchronized.

There are no public fields or other encapsulation violations.

All methods are finite (no infinite loops or unbounded recursion), and so eventually release locks.

All fields are initialized to a consistent state in constructors.

The state of the object is consistent (obeys invariants) at both the beginning and end of each method, even in the presence of exceptions.

For example, consider the following ExpandableArray class, a simplified variant of java.util.Vector.

class ExpandableArray {

protected Object[] data; // the elements
protected int size = 0; // the number of array slots used
// INV: 0 <= size <= data.length

public ExpandableArray(int cap) {
data = new Object[cap];
}

public synchronized int size() {
return size;
}

public synchronized Object get(int i) // subscripted access
throws NoSuchElementException {
if (i < 0 || i >= size )
throw new NoSuchElementException();

return data[i];
}

public synchronized void add(Object x) { // add at end
if (size == data.length) { // need a bigger array
Object[] olddata = data;
data = new Object[3 * (size + 1) / 2];
System.arraycopy(olddata, 0, data, 0, olddata.length);
}
data[size++] = x;
}

public synchronized void removeLast()
throws NoSuchElementException {
if (size == 0)
throw new NoSuchElementException();

data[--size] = null;
}
}

Without synchronization, an instance of this class could not be used reliably in concurrent settings. For example, it could encounter a read/write conflict if processing the accessor at while in the midst of a removeLast operation. And it could encounter a write/write conflict if concurrently performing two add operations, in which case the state of the data array would be very difficult to predict.

2.2.3 Traversal
In fully synchronized classes, you can add another atomic operation just by encasing it in a synchronized method. For the sake of reusability and convenience, it is often a good idea to add small suites of such operations to general-purpose classes or their subclasses. This avoids making clients go through contortions trying to construct atomic versions of commonly used operations out of smaller components. For example, it would be useful to define synchronized versions of removeFirst, prepend, and similar methods to ExpandableArray, as found in java.util.Vector and other collection classes.

However, this strategy doesn't work for another common usage of collections, traversal. A traversal iterates through all elements of a collection and performs some operation on or using each element. Since there are an unbounded number of operations clients might want to apply to the elements of a collection, it is pointless to try to code all of them as synchronized methods.

There are three common solutions to this design problem, aggregate operations, indexed traversal, and versioned iterators, each of which reflect different design trade-offs. (See § 2.4.1.3, § 2.4.4, and § 2.5.1.4 for additional strategies that apply to other kinds of collection classes.) The issues and trade-offs encountered in each approach are seen more generally in the design of many classes using locks.

2.2.3.1 Synchronized aggregate operations
One way to secure traversal is to abstract out the operation being applied to each element so that it can be sent as an argument to a single synchronized applyToAll method. For example:

interface Procedure {
void apply(Object obj);
}

class ExpandableArrayWithApply extends ExpandableArray {

public ExpandableArrayWithApply(int cap) { super(cap); }

synchronized void applyToAll(Procedure p) {
for (int i = 0; i < size; ++i)
p.apply(data[i]);
}
}

This could be used, for example, to print all elements in collection v:

v.applyToAll(new Procedure() {
public void apply(Object obj) {
System.out.println(obj)
}
} );

This approach eliminates potential interference that could occur if other threads attempted to add or remove elements during traversal, but at the expense of possibly holding the lock on the collection for prolonged periods. While this is often acceptable, it may lead to the kinds of liveness and performance problems that motivated the default rule in § 1.1.1.1 saying to release locks when making method calls (here, to apply).

2.2.3.2 Indexed traversal and client-side locking
A second traversal strategy available with ExpandableArray is to require clients to use the indexed accessor methods for traversal; for example:

for (int i = 0; i < v.size(); ++i) // Do not use
System.out.println(v.get(i));

This avoids holding the lock on v while performing each element operation, but at the expense of two synchronized operations (size and at) per element. More importantly, the loop must be rewritten to handle a potential interference problem resulting from the finer locking granularity: It is possible for the check of i < v.size() to succeed but for another thread to remove the current last element before the call to v.get(i). One way to deal with this is to employ client-side locking to preserve atomicity across the size check and access:

for (int i = 0; true; ++i) { // Limited utility
Object obj = null;
synchronized(v) {
if (i < v.size())
obj = v.get(i);
else
break;
}
System.out.println(obj);
}

However, even this can be problematic. For example, if the ExpandableArray class supported methods to rearrange elements, this loop could print the same element twice if v were modified between iterations.

As a more extreme measure, clients can surround the entire traversal with synchronized(v). Again, this is often acceptable but can induce the long-term locking problems seen in synchronized aggregate methods. If the operations on elements are time-consuming, the client can instead first make a copy of the array for traversal purposes:

Object[] snapshot;
synchronized(v) {
snapshot = new Object[v.size()];
for (int i = 0; i < snapshot.length, ++i)
snapshot[i] = v.get(i);
}

for (int i = 0; snapshot.length; ++i) {
System.out.println(snapshot[i]);
}

Client-side locking tends to be used more extensively in non-object-oriented approaches to multithreaded programming. This style is sometimes more flexible, and can be useful in OO systems when instances of a class are designed to be embedded within others (see § 2.4.5) and so must give up internal responsibility for synchronization decisions.

But client-side locking avoids potential interference problems at the expense of encapsulation breakdown. Correctness here relies on special knowledge of the inner workings of the ExpandableArray class that may fail to hold if the class is later modified. Still, this may be acceptable in closed subsystems. Client-side locking can also be a reasonable option when classes document these usages as sanctioned. This also constrains all future modifications and subclasses to support them as well.

2.2.3.3 Versioned iterators
A third approach to traversal is for a collection class to support fast-fail iterators that throw an exception if the collection is modified in the midst of a traversal. The simplest way to arrange this is to maintain a version number that is incremented upon each update to the collection. The iterator can then check this value whenever asked for the next element and throw an exception if it has changed. The version number field should be wide enough that it can never wrap around while a traversal is in progress. An int normally suffices.

This strategy is used in the java.util.Iterator classes in the collections framework. We can apply it here to a subclass of ExpandableArray that updates version numbers as an after-action (see § 1.4.3):

class ExpandableArrayWithIterator extends ExpandableArray {
protected int version = 0;

public ExpandableArrayWithIterator(int cap) { super(cap); }

public synchronized void removeLast()
throws NoSuchElementException {
super.removeLast();
++version; // advertise update
}

public synchronized void add(Object x) {
super.add(x);
++version;
}

public synchronized Iterator iterator() {
return new EAIterator();
}

protected class EAIterator implements Iterator {
protected final int currentVersion;
protected int currentIndex = 0;

EAIterator() { currentVersion = version; }

public Object next() {
synchronized(ExpandableArrayWithIterator.this) {
if (currentVersion != version)
throw new ConcurrentModificationException();
else if (currentIndex == size)
throw new NoSuchElementException();
else
return data[currentIndex++];
}
}

public boolean hasNext() {
synchronized(ExpandableArrayWithIterator.this) {
return (currentIndex < size);
}
}

public void remove() {
// similar
}
}
}

Here, the print loop would be expressed as:

for (Iterator it = v.iterator(); it.hasNext();) {
try {
System.out.println(it.next());
}
catch (NoSuchElementException ex) { /* ... fail ... */ }
catch (ConcurrentModificationException ex) {
/* ... fail ... */
}
}

Even here, choices for dealing with failures are often very limited. A ConcurrentModificationException often signifies unplanned, unwanted interactions among threads that should be remedied rather than patched over.

The versioned iterator approach encapsulates the design choices underlying the data structure, at the price of occasionally undue conservatism. For example, an interleaved add operation would not interfere with the required semantics of a typical traversal, yet would cause an exception to be thrown here. Versioned iterators are still a good default choice for collection classes, in part because it is relatively easy to layer aggregate traversal or client-side locking on top of these iterators, but not vice versa.

2.2.3.4 Visitors
The Visitor pattern described in the Design Patterns book extends the notion of iterators to provide support for clients performing operations on sets of objects connected in arbitrary ways, thus forming the nodes of some kind of tree or graph rather than the sequential list seen in ExpandableArray. (Less relevantly here, the Visitor pattern also supports polymorphic operations on each node.)

The options and concerns for visitors and other extended senses of traversal are similar to, and can sometimes be reduced to, those seen in simple iterators. For example, you might first create a list of all nodes to traverse and then apply any of the above techniques for traversing the list. However, locks here would lock only the list, not the nodes themselves. This is usually the best policy. But if you need to ensure that all of the nodes are locked during the entire traversal, consider forms of confinement (see § 2.3.3) or containment locking (see § 2.4.5).

Conversely, if traversal is arranged by every node supporting a nextNode method, and you do not want to end up simultaneously holding all locks to all nodes encountered during traversal, synchronization of each node must be released before proceeding to the next node, as described in § 2.4.1 and § 2.5.1.4.

2.2.4 Statics and Singletons
As described in the Design Patterns book, a Singleton class intentionally supports only one instance. It is convenient to declare that single instance as a static, in which case both class and instance methods may use the same lock.

Here is one way to define a fully synchronized singleton class that postpones construction of the instance until it is first accessed via the instance method. This class represents a counter that could be used to assign global sequence numbers to objects, transactions, messages, etc., across different classes in an application. (Just to illustrate computation during initialization, the initial value is set to a randomly chosen number with at least 262 positive successors.)

class LazySingletonCounter {
private final long initial;
private long count;

private LazySingletonCounter() {
initial = Math.abs(new java.util.Random().nextLong() / 2);
count = initial;
}

private static LazySingletonCounter s = null;

private static final Object classLock =
LazySingletonCounter.class;

public static LazySingletonCounter instance() {
synchronized(classLock) {
if (s == null)
s = new LazySingletonCounter();
return s;
}
}

public long next() {
synchronized(classLock) { return count++; }
}

public void reset() {
synchronized(classLock) { count = initial; }
}
}

The locking mechanics seen here (or any of several minor variants) prevent situations in which two different threads invoke the instance method at about the same time, causing two instances to be created. Only one of these instances would be bound to s and returned the next time instance is invoked. As discussed in § 2.4.1, in a few cases this and other intentional semantic weakenings might be acceptable; in most cases, however, this would be a serious error.

An easier way to avoid this kind of error is to avoid lazy initialization. Because JVMs perform dynamic loading of classes, there is usually no need to support lazy initialization of singletons. A static field is not initialized until the class is loaded at runtime. While there are no guarantees about exactly when a class will be loaded (beyond that it will be loaded by the time it is accessed by executing code), full initialization of statics is less likely to impose significant start-up overhead than in most other languages. So, unless initialization is both very expensive and rarely needed, it is usually preferable to take the simpler approach of declaring a singleton as a static final field. For example:

class EagerSingletonCounter {
private final long initial;
private long count;

private EagerSingletonCounter() {
initial = Math.abs(new java.util.Random().nextLong() / 2);
count = initial;
}

private static final EagerSingletonCounter s =
new EagerSingletonCounter();

public static EagerSingletonCounter instance() { return s; }
public synchronized long next() { return count++; }
public synchronized void reset() { count = initial; }
}

Simpler yet, if there is no compelling reason to rely on instances, you can instead define and use a version with all static methods, as in:

class StaticCounter {
private static final long initial =
Math.abs(new java.util.Random().nextLong() / 2);
private static long count = initial;
private StaticCounter() { } // disable instance construction
public static synchronized long next() { return count++; }
public static synchronized void reset() { count = initial; }
}

Also, consider using ThreadLocal (see § 2.3.2) rather than a Singleton in situations where it is more appropriate to create one instance of a class per thread than one instance per program.

2.2.5 Deadlock
Although fully synchronized atomic objects are always safe, threads using them are not always live. Consider for example a Cell class containing a method that swaps values with another Cell:

class Cell { // Do not use
private long value;
synchronized long getValue() { return value; }
synchronized void setValue(long v) { value = v; }

synchronized void swapValue(Cell other) {
long t = getValue();
long v = other.getValue();
setValue(v);
other.setValue(t);
}
}

SwapValue is a synchronized multiparty action — one that intrinsically acquires locks on multiple objects. Without further precautions, it is possible for two different threads, one running a.swapValue(b), and the other running b.swapValue(a), to deadlock when encountering the following trace:

Thread 1 Thread 2
acquire lock for a on entering a.swapValue(b)
pass lock for a (since already held) on entering t = getValue() acquire lock for b on entering b.swapValue(a)
block waiting for lock for b on entering v = other.getValue() pass lock for b (since already held) on entering t = getValue()
block waiting for lock for a on entering v = other.getValue()

At this point both threads block forever.


More generally, deadlock is possible when two or more objects are mutually accessible from two or more threads, and each thread holds one lock while trying to obtain another lock already held by another thread.

2.2.6 Resource Ordering
The need to preclude or recover from deadlocks and other liveness failures motivates the use of other exclusion techniques presented in this chapter. However, one simple technique, resource ordering can be applied to classes such as Cell without otherwise altering their structure.

The idea behind resource ordering is to associate a numerical (or any other strictly orderable data type) tag with each object that can be held in a nested synchronized block or method. If synchronization is always performed in least-first order with respect to object tags, then situations can never arise in which one thread has the synchronization lock for x while waiting for y and another has the lock for y while waiting for x. Instead, they will both obtain the locks in the same order, thus avoiding this form of deadlock. More generally, resource ordering can be used whenever there is a need to arbitrarily break symmetry or force precedence in a concurrent design.

In some contexts (see for example § 2.4.5), there may be reasons to impose some specific ordering rules surrounding a set of locks. But in others, you can use any convenient tag for lock-ordering purposes. For example, you may be able to use the value returned by System.identityHashCode. This method always returns the default implementation of Object.hashCode, even if a class overrides the hashCode method. While there is no guarantee that identityHashCode is unique, in practice run-time systems rely on codes to be distinct with a very high probability. To be even safer about it, you could override method hashCode or introduce another tag method to ensure uniqueness in any classes employing resource ordering. For example, you could assign each object a sequence number using one of the classes in § 2.2.4.

One further check, alias detection, can be applied in methods using nested synchronization to handle cases in which two (or more) of the references are actually bound to the same object. For example, in swapValue, you can check whether a Cell is being asked to swap with itself. This kind of check is strictly optional here (but see § 2.5.1). Synchronization lock access is per-thread, not per-invocation. Additional attempts to synchronize on already held objects will still work. However, routine alias-checking is a useful way to forestall downstream functionality, efficiency, and synchronization-based complications. It may be applied before using synchronization surrounding two or more objects unless they are of distinct, unrelated types. (Two references of two unrelated declared types cannot possibly be referring to the same object anyway, so there is no reason to check.)

A better version of swapValue, applying both resource ordering and alias detection, can be written as:

public void swapValue(Cell other) {
if (other == this) // alias check
return;
else if (System.identityHashCode(this) <
System.identityHashCode(other))
this.doSwapValue(other);
else
other.doSwapValue(this);
}

protected synchronized void doSwapValue(Cell other) {
// same as original public version:
long t = getValue();
long v = other.getValue();
setValue(v);
other.setValue(t);
}

As a minor efficiency tweak, we could further streamline the code inside doSwapValue first to acquire the necessary locks, and then directly access the value fields. This avoids a self-call to a synchronized method while already holding the required lock, at the minor expense of adding lines of code that would need to be changed if the nature of the fields were ever modified:

// slightly faster version
protected synchronized void doSwapValue(Cell other) {
synchronized(other) {
long t = value;
value = other.value;
other.value = t;
}
}

Note that the lock for this is obtained via the synchronized method qualifier, but the lock for other is explicitly acquired. A further, very tiny (perhaps nonexistent) performance improvement might be obtained by folding the code in doSwapValue into swapValue, remembering to acquire both locks explicitly.

Lock-ordering problems are by no means restricted to methods using nested synchronization. The issue arises in any code sequence in which a synchronized method holding the lock on one object in turn calls a synchronized method on another object. However, there is less opportunity to apply resource ordering in cascaded calls: In the general case, one object cannot know for sure which other objects will be involved in downstream calls and whether they require synchronization. This is one reason that deadlock can be such a hard problem in open systems (see § 2.5) when you cannot release synchronization during calls (see § 2.4.1).

2.2.7 The Java Memory Model
Consider the tiny class, defined without any synchronization:

final class SetCheck {
private int a = 0;
private long b = 0;

void set() {
a = 1;
b = -1;
}

boolean check() {
return ((b == 0) ||
(b == -1 && a == 1));
}
}

In a purely sequential language, the method check could never return false. This holds even though compilers, run-time systems, and hardware might process this code in a way that you might not intuitively expect. For example, any of the following might apply to the execution of method set:

The compiler may rearrange the order of the statements, so b may be assigned before a. If the method is inlined, the compiler may further rearrange the orders with respect to yet other statements.

The processor may rearrange the execution order of machine instructions corresponding to the statements, or even execute them at the same time.

The memory system (as governed by cache control units) may rearrange the order in which writes are committed to memory cells corresponding to the variables. These writes may overlap with other computations and memory actions.

The compiler, processor, and/or memory system may interleave the machine-level effects of the two statements. For example on a 32-bit machine, the high-order word of b may be written first, followed by the write to a, followed by the write to the low-order word of b.

The compiler, processor, and/or memory system may cause the memory cells representing the variables not to be updated until sometime after (if ever) a subsequent check is called, but instead to maintain the corresponding values (for example in CPU registers) in such a way that the code still has the intended effect.

In a sequential language, none of this can matter so long as program execution obeys as-if-serial semantics[2] . Sequential programs cannot depend on the internal processing details of statements within simple code blocks, so they are free to be manipulated in all these ways. This provides essential flexibility for compilers and machines. Exploitation of such opportunities (via pipelined superscalar CPUs, multilevel caches, load/store balancing, interprocedural register allocation, and so on) is responsible for a significant amount of the massive improvements in execution speed seen in computing over the past decade. The as-if-serial property of these manipulations shields sequential programmers from needing to know if or how they take place. Programmers who never create their own threads are almost never impacted by these issues.

[2] Somewhat more precisely, as-if-serial (also known as program order) semantics can be defined as any execution traversal of the graph formed by ordering only those operations that have value or control dependencies with respect to each other under a language's base expression and statement semantics.

Things are different in concurrent programming. Here, it is entirely possible for check to be called in one thread while set is being executed in another, in which case the check might be “spying” on the optimized execution of set. And if any of the above manipulations occur, it is possible for check to return false. For example, as detailed below, check could read a value for the long b that is neither 0 nor -1, but instead a half-written in-between value. Also, out-of-order execution of the statements in set may cause check to read b as -1 but then read a as still 0.

In other words, not only may concurrent executions be interleaved, but they may also be reordered and otherwise manipulated in an optimized form that bears little resemblance to their source code. As compiler and run-time technology matures and multiprocessors become more prevalent, such phenomena become more common. They can lead to surprising results for programmers with backgrounds in sequential programming (in other words, just about all programmers) who have never been exposed to the underlying execution properties of allegedly sequential code. This can be the source of subtle concurrent programming errors.

In almost all cases, there is an obvious, simple way to avoid contemplation of all the complexities arising in concurrent programs due to optimized execution mechanics: Use synchronization. For example, if both methods in class SetCheck are declared as synchronized, then you can be sure that no internal processing details can affect the intended outcome of this code.

But sometimes you cannot or do not want to use synchronization. Or perhaps you must reason about someone else's code that does not use it. In these cases you must rely on the minimal guarantees about resulting semantics spelled out by the Java Memory Model. This model allows the kinds of manipulations listed above, but bounds their potential effects on execution semantics and additionally points to some techniques programmers can use to control some aspects of these semantics (most of which are discussed in § 2.4).

The Java Memory Model is part of The Java? Language Specification, described primarily in JLS chapter 17. Here, we discuss only the basic motivation, properties, and programming consequences of the model. The treatment here reflects a few clarifications and updates that are missing from the first edition of JLS[3] .

[3] As of this writing, the memory model and other relevant sections of JLS are still being updated to cover the Java 2 Platform. Please check the online supplement for any changes that impact the material in this section.

The assumptions underlying the model can be viewed as an idealization of a standard SMP machine of the sort described in § 1.2.4:


For purposes of the model, every thread can be thought of as running on a different CPU from any other thread. Even on multiprocessors, this is infrequent in practice, but the fact that this CPU-per-thread mapping is among the legal ways to implement threads accounts for some of the model's initially surprising properties. For example, because CPUs hold registers that cannot be directly accessed by other CPUs, the model must allow for cases in which one thread does not know about values being manipulated by another thread. However, the impact of the model is by no means restricted to multiprocessors. The actions of compilers and processors can lead to identical concerns even on single-CPU systems.

The model does not specifically address whether the kinds of execution tactics discussed above are performed by compilers, CPUs, cache controllers, or any other mechanism. It does not even discuss them in terms of classes, objects, and methods familiar to programmers. Instead, the model defines an abstract relation between threads and main memory. Every thread is defined to have a working memory (an abstraction of caches and registers) in which to store values. The model guarantees a few properties surrounding the interactions of instruction sequences corresponding to methods and memory cells corresponding to fields. Most rules are phrased in terms of when values must be transferred between the main memory and per-thread working memory. The rules address three intertwined issues:

Atomicity. Which instructions must have indivisible effects. For purposes of the model, these rules need to be stated only for simple reads and writes of memory cells representing fields — instance and static variables, also including array elements, but not including local variables inside methods.

Visibility. Under what conditions the effects of one thread are visible to another. The effects of interest here are writes to fields, as seen via reads of those fields.

Ordering. Under what conditions the effects of operations can appear out of order to any given thread. The main ordering issues surround reads and writes associated with sequences of assignment statements.

When synchronization is used consistently, each of these properties has a simple characterization: All changes made in one synchronized method or block are atomic and visible with respect to other synchronized methods and blocks employing the same lock, and processing of synchronized methods or blocks within any given thread is in program-specified order. Even though processing of statements within blocks may be out of order, t his cannot matter to other threads employing synchronization.

When synchronization is not used or is used inconsistently, answers become more complex. The guarantees made by the memory model are weaker than most programmers intuitively expect, and are also weaker than those typically provided on any given JVM implementation. This imposes additional obligations on programmers attempting to ensure the object consistency relations that lie at the heart of exclusion practices: Objects must maintain invariants as seen by all threads that rely on them, not just by the thread performing any given state modification.

The most important rules and properties specified by the model are discussed below.

2.2.7.1 Atomicity
Accesses and updates to the memory cells corresponding to fields of any type except long or double are guaranteed to be atomic. This includes fields serving as references to other objects. Additionally, atomicity extends to volatile long and double. (Even though non-volatile longs and doubles are not guaranteed atomic, they are of course allowed to be.)

Atomicity guarantees ensure that when a non-long/double field is used in an expression, you will obtain either its initial value or some value that was written by some thread, but not some jumble of bits resulting from two or more threads both trying to write values at the same time. However, as seen below, atomicity alone does not guarantee that you will get the value most recently written by any thread. For this reason, atomicity guarantees per se normally have little impact on concurrent program design.

2.2.7.2 Visibility
Changes to fields made by one thread are guaranteed to be visible to other threads only under the following conditions:

A writing thread releases a synchronization lock and a reading thread subsequently acquires that same synchronization lock.

In essence, releasing a lock forces a flush of all writes from working memory employed by the thread, and acquiring a lock forces a (re)load of the values of accessible fields. While lock actions provide exclusion only for the operations performed within a synchronized method or block, these memory effects are defined to cover all fields used by the thread performing the action.

Note the double meaning of synchronized: it deals with locks that permit higher-level synchronization protocols, while at the same time dealing with the memory system (sometimes via low-level memory barrier machine instructions) to keep value representations in synch across threads. This reflects one way in which concurrent programming bears more similarity to distributed programming than to sequential programming. The latter sense of synchronized may be viewed as a mechanism by which a method running in one thread indicates that it is willing to send and/or receive changes to variables to and from methods running in other threads. From this point of view, using locks and passing messages might be seen merely as syntactic variants of each other.

If a field is declared as volatile, any value written to it is flushed and made visible by the writer thread before the writer thread performs any further memory operation (i.e., for the purposes at hand it is flushed immediately). Reader threads must reload the values of volatile fields upon each access.

The first time a thread accesses a field of an object, it sees either the initial value[4] of the field or a value since written by some other thread.

[4] As of this writing, the JLS does not yet clearly state that the visible initial value read for an initialized final field is the value assigned in its initializer or constructor. However, this anticipated clarification is assumed throughout this book. The visible initial default values of non-final fields are zero for scalars and null for references.

Among other consequences, it is bad practice to make available the reference to an incompletely constructed object (see § 2.1.2). It can also be risky to start new threads inside a constructor, especially in a class that may be subclassed. Thread.start has the same memory effects as a lock release by the thread calling start, followed by a lock acquire by the started thread. If a Runnable superclass invokes new Thread(this).start() before subclass constructors execute, then the object might not be fully initialized when the run method executes. Similarly, if you create and start a new thread T and then create an object X used by thread T, you cannot be sure that the fields of X will be visible to T unless you employ synchronization surrounding all references to object X. Or, when applicable, you can create X before starting T.

As a thread terminates, all written variables are flushed to main memory.

For example, if one thread synchronizes on the termination of another thread using Thread.join, then it is guaranteed to see the effects made by that thread (see § 4.3.2).

Note that visibility problems never arise when passing references to objects across methods in the same thread.

The memory model guarantees that, given the eventual occurrence of the above operations, a particular update to a particular field made by one thread will eventually be visible to another. But eventually can be an arbitrarily long time. Long stretches of code in threads that use no synchronization can be hopelessly out of synch with other threads with respect to values of fields. In particular, it is always wrong to write loops waiting for values written by other threads unless the fields are volatile or accessed via synchronization (see § 3.2.6).

The model also allows inconsistent visibility in the absence of synchronization. For example, it is possible to obtain a fresh value for one field of an object, but a stale value for another. Similarly, it is possible to read a fresh, updated value of a reference variable, but a stale value of one of the fields of the object now being referenced.

However, the rules do not require visibility failures across threads, they merely allow these failures to occur. This is one aspect of the fact that not using synchronization in multithreaded code doesn't guarantee safety violations, it just allows them. On most current JVM implementations and platforms, even those employing multiple processors, detectable visibility failures rarely occur. The use of common caches across threads sharing a CPU, the lack of aggressive compiler-based optimizations, and the presence of strong cache consistency hardware often cause values to act as if they propagate immediately among threads. This makes testing for freedom from visibility-based errors impractical, since such errors might occur extremely rarely, or only on platforms you do not have access to, or only on those that have not even been built yet. These same comments apply to multithreaded safety failures more generally. Concurrent programs that do not use synchronization fail for many reasons, including memory consistency problems.

2.2.7.3 Ordering
Ordering rules fall under two cases, within-thread and between-thread:

From the point of view of the thread performing the actions in a method, instructions proceed in the normal as-if-serial manner that applies in sequential programming languages.

From the point of view of other threads that might be “spying” on this thread by concurrently running unsynchronized methods, almost anything can happen. The only useful constraint is that the relative orderings of synchronized methods and blocks, as well as operations on volatile fields, are always preserved.

Again, these are only the minimal guaranteed properties. In any given program or platform, you may find stricter orderings. But you cannot rely on them, and you may find it difficult to test for code that would fail on JVM implementations that have different properties but still conform to the rules.

Note that the within-thread point of view is implicitly adopted in all other discussions of semantics in JLS. For example, arithmetic expression evaluation is performed in left-to-right order (JLS section 15.6) as viewed by the thread performing the operations, but not necessarily as viewed by other threads.

The within-thread as-if-serial property is helpful only when only one thread at a time is manipulating variables, due to synchronization, structural exclusion, or pure chance. When multiple threads are all running unsynchronized code that reads and writes common fields, then arbitrary interleavings, atomicity failures, race conditions, and visibility failures may result in execution patterns that make the notion of as-if-serial just about meaningless with respect to any given thread.

Even though JLS addresses some particular legal and illegal reorderings that can occur, interactions with these other issues reduce practical guarantees to saying that the results may reflect just about any possible interleaving of just about any possible reordering. So there is no point in trying to reason about the ordering properties of such code.

2.2.7.4 Volatile
In terms of atomicity, visibility, and ordering, declaring a field as volatile is nearly identical in effect to using a little fully synchronized class protecting only that field via get/set methods, as in:

final class VFloat {
private float value;

final synchronized void set(float f) { value = f; }
final synchronized float get() { return value; }
}

Declaring a field as volatile differs only in that no locking is involved. In particular, composite read/write operations such as the “++'' operation on volatile variables are not performed atomically.

Also, ordering and visibility effects surround only the single access or update to the volatile field itself. Declaring a reference field as volatile does not ensure visibility of non-volatile fields that are accessed via this reference. Similarly, declaring an array field as volatile does not ensure visibility of its elements. Volatility cannot be manually propagated for arrays because array elements themselves cannot be declared as volatile.

Because no locking is involved, declaring fields as volatile is likely to be cheaper than using synchronization, or at least no more expensive. However, if volatile fields are accessed frequently inside methods, their use is likely to lead to slower performance than would locking the entire methods.

Declaring fields as volatile can be useful when you do not need locking for any other reason, yet values must be accurately accessible across multiple threads. This may occur when:

The field need not obey any invariants with respect to others.

Writes to the field do not depend on its current value.

No thread ever writes an illegal value with respect to intended semantics.

The actions of readers do not depend on values of other non-volatile fields.

Using volatile fields can make sense when it is somehow known that only one thread can change a field, but many other threads are allowed to read it at any time. For example, a Thermometer class might declare its temperature field as volatile. As discussed in § 3.4.2, a volatile can be useful as a completion flag. Additional examples are illustrated in § 4.4, where the use of lightweight executable frameworks automates some aspects of synchronization, but volatile declarations are needed to ensure that result field values are visible across tasks.

2.2.8 Further Readings
Features of computer architectures that impact multithreaded programs are described in:

Schimmel, Curt. UNIX Systems for Modern Architectures Symmetric Multiprocessing and Caching for Kernel Programmers, Addison-Wesley, 1994.

Patterson, David, and John Hennessy. Computer Organization and Design: The Hardware/Software Interface, Morgan Kaufmann, 1997. See also its online supplement with links to further resources on specific machine architectures.

Memory consistency models are the subject of increasing attention as both multiprocessors and multithreaded programs become more common and their interactions become more of a concern. At least with respect to locking, the Java memory model is closest to the family of release consistency models. For an overview, see:

Adve, Sarita and K. Gharachorloo. “Shared Memory Consistency Models: A Tutorial”, IEEE Computer, December 1996, 66-76. See also follow-ups, including: “Recent Advances in Memory Consistency Models for Hardware Shared-Memory Systems” Proceedings of the IEEE, special issue on distributed shared memory, 1999.


dingligang edited on 2003-12-21 14:42

定风波
莫听穿林打叶声,
何妨吟啸且徐行。
竹杖芒鞋轻胜马,谁怕?
一蓑烟雨任平生。
料峭春寒吹酒醒,微冷!
山头斜照却相迎。
回首向来潇洒处,归去
也无风雨也无晴。

话题树型展开
人气 标题 作者 字数 发贴时间
7329 如何对一个变量进行同步与互斥操作? RichardCheung 40 2003-12-12 17:54
6712 Re:如何对一个变量进行同步与互斥操作? whoami13 69 2003-12-12 21:08
6646 Re:如何对一个变量进行同步与互斥操作? XiEJEsEn 2 2003-12-15 16:09
6572 Re:如何对一个变量进行同步与互斥操作? RichardCheung 3 2003-12-19 10:06
6761 Re:如何对一个变量进行同步与互斥操作? dissip 102 2003-12-19 15:15
7193 Re:如何对一个变量进行同步与互斥操作? dingligang 49783 2003-12-21 14:35
7082 Re:如何对一个变量进行同步与互斥操作? dingligang 17 2003-12-21 14:40
6520 Re:如何对一个变量进行同步与互斥操作? RichardCheung 6 2003-12-21 16:50
6826 Re:如何对一个变量进行同步与互斥操作? mochow 18 2003-12-22 10:29
6643 Re:如何对一个变量进行同步与互斥操作? RichardCheung 4 2003-12-13 09:35
6801 Re:如何对一个变量进行同步与互斥操作? jameszhang 28 2003-12-13 10:38
6804 Re:如何对一个变量进行同步与互斥操作? RichardCheung 798 2003-12-13 17:14
6654 Re:如何对一个变量进行同步与互斥操作? RichardCheung 29 2003-12-13 17:19
6837 Re:如何对一个变量进行同步与互斥操作? mochow 46 2003-12-15 10:00
6709 Re:如何对一个变量进行同步与互斥操作? xugreat 43 2003-12-15 10:08
6863 Re:如何对一个变量进行同步与互斥操作? mochow 74 2003-12-15 10:18
7089 Re:如何对一个变量进行同步与互斥操作? jigsaw 7 2003-12-15 15:13

flat modethreaded modego to previous topicgo to next topicgo to back
  已读帖子
  新的帖子
  被删除的帖子
Jump to the top of page

   Powered by Jute Powerful Forum® Version Jute 1.5.6 Ent
Copyright © 2002-2021 Cjsdn Team. All Righits Reserved. 闽ICP备05005120号-1
客服电话 18559299278    客服信箱 714923@qq.com    客服QQ 714923