Java -- Concurrent Fields

"volatile" Field:

The "volatile" keyword offers a lock-free mechanism for synchronizing access to an instance field. If you declare a field as "volitale", then the compiler and the virtual machine take into account that the field may be concurrently updated by another thread.

For example, declare a field as "volatile"

private volatile boolean done;
public boolean isDone() {
    return done;
}
public void setDone(boolean done) {
    this.done = done;
}

The compiler will insert the appropriate code to ensure that a change to the "done" variable in one thread is visible from any other thread that reads the variable.

That is you can declare shared variables as "volatile" provided you perform no operations other than assignment.


"final" Field:

There is one other situation in which it is safe to access a thread field -- when it is declared "final" .  Consider

final Map<String, Double> accounts = new HashMap<>();

Other threads get to see the "accounts" variable after the constructor has finished.

Without using "final", there would be no guarantee that other threads would see the updated value of "accounts" -- they might see "null", not the constructed "HashMap".

Of course, the operations on the map are not thread safe. If multiple threads mutate and read the map, you still need synchronization.


Atomics

There are number of classes in the "java.util.concurrent.atomic" package that use efficient machine-level instructions to guerantee atomicity of other operations without using locks.

"AtomicInteger" class has methods "incrementAndGet" and "decrementAndGet" that atomically increment or decrement an integer. For example, you can safely generate a sequence of numbers like this:

public static AtomicLong nextNumber = new AtomicLong();
//In some thread ...
long id = nextNumber.incrementAndGet();

The "incrementAndGet" method atomically increments the "AtomicLong" and returns the post-increment value. That is, the operations of getting the value, adding 1, setting it, and producing the new value cannot be interrupted. It is guaranteed that the correct value is computed and returned, even if multiple threads access the same instance concurrently. There are methods for atomically setting, adding, and subtracting values, but if you want to make more complex update, you have to use the "compareAndSet" method. For example, suppose you want to keep track of the largest value that is observed by different threads. The following want work:

public static AtomicLong largest = new AtomicLong();
// In some thread ...
largest.set(Math.max(largest.get(), observed)); // Error -- race condition!

This update is not atomic. Instead, compute the new value and use "compareAndSet" in a loop:

do {
    oldValue = largest.get();
    newValue = Math.max(oldValue, observed);
} while (!largest.compareAndSet(oldValue, newValue));

If another thread is also updating "largest", it is possible that it has beat this thread to it. Then "compareAndSet" will return "false" without setting the new value. In that case, the loop tries again, reading the updated value and trying to change it. Eventually, it will succeed replacing the existing value with the new one. This sounds tedious, but the "compareAndSet" method maps to a processor operation that is faster than using a lock. In Java SE 8, you don't have to write the loop boilerplate any more. Instead, you provide a lambda expression for updating the variable, and the update is done for you. In our example, we can call:

largest.updateAndGet(x -> Math.max(x, observed));

or

largest.accumulateAndGet(observed, Math::max);

The "accumulateAndGet" method takes a binary operator that is used to combine the atomic value and the given argument. There are also methods "getAndUpdate" and "getAndAccumulate" that return the old value.

NOTE: These methods are also provided for the classes "AtomicInteger", "AtomicIntegerArray", "AtomicIntegerFieldUpdater", "AtomicLongArray", "AtomicLongUpdater", "AtomicReference", "AtomicReferenceArray", and "AtomicReferenceFieldUpdater".

When you have a large number of threads accessing the same atomic values, performance suffers because the optimistic updates require to many retries. Java SE 8 provides classes "LongAdder" and "LongAccumulator" to solve this problem. A "LongAdder" is composed of multiple variables whose collective sum is the current value. Multiple threads can update defferent summands, and new summands are automatically provided when the number of threads increases. This is efficient in the common situation where the value of the sum is not needed until after all work has been done. The performance improvement can be substantial. If anticipate high contention, you should simply use a "LongAdder" instead of an "AtomicLong". The method names a slightly defferent. Call "increment" to increment a counter or "add" to add a quantity, and "sum" to retrieve the total.

NOTE: Of course, the "increment" method does not return the old value. Doing that would undo the efficiency gain of splitting the sum into multiple summands.

The "LongAccumulator" generalizes this idea to an arbitrary accumulation operation. In the constructor, you provide the operation, as well as its neutral element. To incorporate new values, call "accumulate". Call "get" to obtain the current value. The following has the same effect as a "LongAdder":

LongAccumulator adder = new LongAccumulator(Long:sum, 0);
// In some thread ...
adder.accumulate(value);

Internally, the accumulator has variables a1, a2, a3, ..., an. Each variable is initialized with the neutral element (0 in this our example). When "accumulate" is called with value v, then one of them is atomically updated as "a1 = a1 op v" where "op" is the accumulation operation written in infix form. In our example, a call to "accumulate" computes "ai = ai + v" for some i. The result of "get" is "a1 op a2 op ... op an". In our example, that is the sum of the accumulators, "a1 + a2 + ... + an". If you choose a different operation, you can compute maximum or minimum. In general, the operation must be associative and commutative. That means that the final result must be independent of the order in which the intermediate values were combined.

There are also "DounbleAdder" and "DoubleAccumulator" that work in the same way, except with "double" values.

猜你喜欢

转载自blog.csdn.net/liangking81/article/details/80784266
今日推荐