This article is an extract of my own learning during the work assignments. Intended audience is the regular Java Developers who want to refer and refresh the ways thread safe program should be written.
Avoiding a race condition while working on a Multi Threaded Java application has always been a challenge. Though Java has all the techniques available to do that, some out of touch Java Developers might use wrong methods to code. Even if its coded with utmost care and satisfaction - still you would find some 'Monday morning quarterback' to stand and question your intentions!!
And it can be difficult to support yourself if you are out of touch lately
Hope this blog would help!
There are three concepts which are fundamental to correct concurrent programming. When a concurrent program is not correctly written, the errors tend to fall into one of the three categories:
- Atomicity - deals with actions and sets of actions which are to be executed either all-or-nothing.
- Visibility - determines when the effects of one thread can be seen by another.
- Ordering - determines when actions in one thread can be seen to occur out of order with respect to another. Note - When the compiler generates byte code, it can reorder certain program statements on the pretext of optimization.
public class Rating{
private int likeCount = 0;
public void incrementLikeCount(){
++likeCount;
}
}
The issue with this code is obvious, if two threads (
two users LIKING the page simultaneously) try to
incrementLikeCount at same time, both may see the
likeCount as 0 and both may just increment it to 1. Thus, loosing a LIKE!
A code section which must not be executed by more than one thread at same time is known as
Critical Section. There is a need to control the access to the critical section. Here the method
incrementLikeCount() contains the critical section (
++likeCount) as it increments
likeCount and can be called by multiple threads.
Volatile:
To resolve this, the most faulty way is to just use
volatile
keyword:
public class Rating{
private volatile int likeCount = 0;
public void incrementLikeCount(){
++likeCount;
}
}
Before going furter, lets see as how the increment of a
volatile integer works internally,
++likeCount implies:
temp1 = likeCount;
temp2 = temp1 + 1;
likeCount = temp2;
When a
volatile is written to (
++likeCount), the value is written to main memory and not to local processor cache and all the other caches of other cores are informed of this change by message passing. Post Java 5,
volatile ensures
Ordering, of code instructions.
Visibility, i.e. When one thread writes to a volatile variable, and another thread sees that write, the first thread is telling the second about all of the contents of memory up until it performed the write to that volatile variable. But, volatile doesn't ensures
Atomicity, as it does nothing to control the access to the critical section!!
Please read more on volatile here - Volatile Does Not Mean Atomic! [By Jeremy Manson].
As stated above, when one thread writes to a
volatile variable, essentially its sharing its memory space with other threads showing them all of the contents of its memory up until it performed the write to that volatile variable. Hence, using
volatile variables might be considered as a security risk.
Please read more on this here - What Volatile Means in Java [By Jeremy Manson]
So to finally state it once again,
volatile is not a solution to race conditions.
Synchronized:
Next way on list is to use
synchronized blocks and methods:
public class Rating{
private volatile int likeCount = 0;
public synchronized void incrementLikeCount(){
++likeCount;
}
}
The synchronized keyword is used in two primary contexts:
- As a method modifier to mark a method that it can only be executed by one thread at a time.
- By declaring a code block as a critical section – one that’s only available to a single thread at any given point in time.
Synchronized code blocks are implemented using two dedicated bytecode instructions, which are part of the official specification – MonitorEnter and MonitorExit. The compiler adds an implicit local variable to the method to hold the value of the locked object between the corresponding enter and exit calls.
Note 1: synchronized ensures thread safety of the code by locking the code and providing the access to threads one at a time. Hence, the disadvantage is the 'performance' of the application.
Note 2: We can try using 'double-checked-locking' in certain cases to lessen the performance degradation. In our current example we can't.
Note 3: We are still using volatile here, with synchronized!
Atomic Instructions:
Next way on list is use Java Concurrent API for Atomic Classes:
public class Rating{
private AtomicInteger likeCount = new AtomicInteger(0);
public void incrementLikeCount(){
likeCount.incrementAndGet();
}
}
AtomicInteger actually makes use of volatile and
CAS (Compare And Sweep) for thread-safe implementation. CAS does not make use of locking rather it is very optimistic in nature. It follows these steps:
- Compare the value of the primitive to the value we have got in hand.
- If the values do not match it means some thread in between has changed the value. Else it will go ahead and swap the value with new value.
When there is a high contention and a large number of threads want to update the same atomic variable. In that case there is a possibility that locking will outperform the atomic variables. There is one more construct introduced in Java 8, LongAdder. As per the documentation:
This class is usually preferable to AtomicLong when multiple threads update a common sum that is used for purposes such as collecting statistics, not for fine-grained synchronization control. Under low update contention, the two classes have similar characteristics. But under high contention, expected throughput of this class is significantly higher, at the expense of higher space consumption.
So LongAdder is not always a replacement for AtomicLong:
- When no contention is present AtomicLong performs better.
- LongAdder will allocate Cells (a final class declared in abstract class Striped64) to avoid contention which consumes memory. So in case we have a tight memory budget we should prefer AtomicLong.
Biased Locking:
Since most Java objects are locked by at most one thread during their lifetime, that thread can bias an object toward itself. Once an Object is biased to a thread, that thread can subsequently lock and unlock the object without falling-back to other expensive techniques as atomic instructions or normal conventional locking. Biased locking is strictly a response to the latency of CAS (Compare And Sweep). It's important to note that CAS incurs local latency, but does not impact scalability on modern processors.
Before Java 6, following JVM option needs to be enabled for Biased Locking:
-XX:+UseBiasedLocking Enables a technique for improving the performance of uncontended synchronization. An object is "biased" toward the thread which first acquires its monitor via a monitor enter bytecode or synchronized method invocation; subsequent monitor-related operations performed by that thread are relatively much faster on multiprocessor machines.
An object can be biased toward at most one thread at any given time. That thread is termed as "
bias holding thread". If another thread tries to acquire a biased object, we need to revoke the bias from the original thread. This is accomplished by the VM Operation
RevokeBias.
To understand the concept of Bias Locking please read - Biased Locking in HotSpot [By David Dice].
To read more on RevokeBias - Java VM: Safepoint for RevokeBias.
Thanks for the read!!