When solving software engineering challenges, in most cases there is no single solution. Rather than, there are always a bunch of them. Each approach is unique and has some limitations.
Last time I wrote about synchronized keyword as a way to avoid race conditions. You can find the article below.
Let's have a quick look at how we addressed the concurrency issue previously.
public class Counter {
private int cnt = 0;
public synchronized int getCnt() {
return cnt;
}
public synchronized void inc() {
++cnt;
}
}
We can see that to avoid race conditions, we decorated each method with synchronized keyword.
As a matter of fact, synchronized is not the only way to go with. As long as we need to update a single member and make it consistent across other threads, we can leverage from atomics in JAVA that supports lock-free thread-safe programming. The standard library provides plenty of them.
Class Description AtomicBoolean A boolean value that may be updated atomically. AtomicInteger An int value that may be updated atomically. AtomicIntegerArray An int array in which elements may be updated atomically. AtomicIntegerFieldUpdater A reflection-based utility that enables atomic updates to designated volatile int fields of designated classes. AtomicLong A long value that may be updated atomically. AtomicLongArray A long array in which elements may be updated atomically. AtomicLongFieldUpdater A reflection-based utility that enables atomic updates to designated volatile long fields of designated classes. AtomicMarkableReference An AtomicMarkableReference maintains an object reference along with a mark bit, that can be updated atomically. AtomicReference An object reference that may be updated atomically. AtomicReferenceArray An array of object references in which elements may be updated atomically. AtomicReferenceFieldUpdater<T,V> A reflection-based utility that enables atomic updates to designated volatile reference fields of designated classes. AtomicStampedReference An AtomicStampedReference maintains an object reference along with an integer "stamp", that can be updated atomically. DoubleAccumulator One or more variables that together maintain a running double value updated using a supplied function. DoubleAdder One or more variables that together maintain an initially zero double sum. LongAccumulator One or more variables that together maintain a running long value updated using a supplied function. LongAdder One or more variables that together maintain an initially zero long sum.
So let's try to fix Counter using AtomicInteger instead of synchronized.
public class Counter {
private final AtomicInteger cnt = new AtomicInteger();
public int getCnt() {
return cnt.get();
}
public void inc() {
cnt.incrementAndGet();
}
}
We can notice that we stripped methods from synchronized keyword. Now, cnt is AtomicInteger instead of plain int. To obtain its value, we use get() method. In the case of inc() method, we call incrementAndGet() instead of ++.
df55aa7e-7984-4abf-b6f3-20d6d5750cfb
48634da1-c62d-4c3d-abbd-7efd8f4f53e0
68b5a252-964a-47a8-b8cf-7edfd43b0a13
Processed events: 10000
Implementation is pretty straightforward and fixes race conditions as well. We can notice that basic operations have their equivalence in atomic methods.
Basic operation Atomic equivalent ++cnt cnt.incrementAndGet() cnt++ cnt.getAndIncrement() --cnt cnt.decrementAndGet() cnt-- cnt.getAndDecrement() cnt = x cnt.set(x) x = cnt x = cnt.get()
The specifications of these methods enable implementations to employ efficient machine-level atomic instructions that are available on contemporary processors. However, on some platforms, support may entail some form of internal locking. Thus, the methods are not strictly guaranteed to be non-blocking -- a thread may block transiently before performing the operation.
To sum up, atomic classes are designed primarily as building blocks for implementing non-blocking data structures and related infrastructure classes. They apply only when critical updates for an object are confined to a single variable.