Java | Multithreading Part 2 : Atomicity, Visibility, Ordering
In this article, we will understand how we can fix problems with atomic-ness, visibility, and ordering of variables in Java.
In the previous part, we learned: Java Memory Model and how it maps to Computer Hardware Memory
Let’s look at an example where we have a Work
class and two threads ThreadA
and ThreadB
.
ThreadA
call the work()
method and at the same time ThreadB
call the stopWork()
method.
We get a situation in which the work()
method will be executed constantly.
In this situation, the volatile
keyword will help us.
volatile
guarantees that each read of a volatile variable will be read from the computer’s main memory and not the CPU cache, and that each write to a volatile variable will be written to main memory and not just the CPU cache.
The Java volatile
keyword cannot be used with a method or class, and it can only be used with a variable.
Let’s fix our code a bit.
class Worker {
volatile boolean done = false;
void work() {
while (!done) {
// do work
}
}
void stopWork() {
done = true;
}
}
Now the CPU will not cache the done variable. It will always be read from memory.
From the first article we learned:
Java allows 64-bit long and double values to be treated as two 32-bit values. A long write is a 64-bit write operation that is executed as two separate 32-bit operations.
public class Balance {
private long balance;
void set(long value) {
// This is risky in multi threaded environment.
balance = value;
}
long get() {
return balance;
}
}
When solving this problem, we can use volatile
.
What our code will look like:
public class Balance {
private volatile long balance;
void set(long value) {
balance = value;
}
long get() {
return balance;
}
}
volatile
enforces the use of a single instance of a variable, but does not guarantee atomicity.
For example, the operation count++
will not become atomic simply because count is declared volatile
.
Consider the following example:
public class Counter {
private volatile int counter;
public int incrementCounter() {
return counter++;
}
public int getCounter() {
return counter;
}
}
counter++;
This is Non-atomic operation. Because at first, we read the counter
variable, and then we will increment the value.
At the moment when we called the incrementCounter()
method and read the variable, at this time another thread can call the getCounter()
method and get irrelevant data.
AtomicInteger
provides an atomic method to perform such complex operations atomically, such as getAndIncrement()
— atomic replacement for the increment operator, it can be used to atomically increment the current value by one.
public class Counter {
private final AtomicInteger counter = new AtomicInteger(0);
public int incrementCounter() {
return counter.incrementAndGet();
}
public int getCounter() {
return counter.getAndDecrement();
}
}
Atomic
versions are constructed similarly for other data types.
Java provides a set of: AtomicInteger AtomicLong AtomicBoolean AtomicDouble.
What are the differences between Atomic and volatile?
Let’s look for example:
public class Service {
volatile boolean initializationStarted = false;
void initialize() {
if (initializationStarted) {
initializationStarted = true;
initializeService();
}
}
void initializeService() {
// todo some code
}
}
volatile
variable will be guaranteed to be read from memory, but volatile does not guarantee atomic read and write to the variable.
If two threads simultaneously call the initialize()
method, then we can get a situation where both read the local variable initializationStarted as false.
Let’s look at an AtomicBoolean example:
public class Service {
final AtomicBoolean initializationStarted = new AtomicBoolean(false);
void initialize() {
if (initializationStarted.compareAndSet(false, true)) {
initializeService();
}
}
void initializeService() {
// todo some code
}
}
AtomicBoolean helps us avoid the situation described above.
If we refer to class Atomic we will see two methods:
java.util.concurrent.Atomic*.compareAndSwap()
java.util.concurrent.Atomic*.weakCompareAndSwap()
On some platforms, the weakCompareAndSwap
version may be more efficient than compareAndSet
in the normal case,, but not supported by all architectures and not always efficient operation.
Method weakCompareAndSwap
does not establish a happens-before relationship, so it’s simply that there is no guarantee that the modification it causes is visible in other threads. All you get in this case is the guarantee that the compare-and-set operation is atomic, but with no guarantees about the visibility of the (potentially) new value.
compareAndSwap
and all other read-and-update operations such as getAndIncrement have the memory effects of both reading and writing volatile variables.
Probably don’t need to use weakCompareAndSet
method, but instead should just use compareAndSet
;
As there are few cases where weakCompareAndSet is faster than compareAndSet, and there are a number of cases in which trying to optimizing your code by using weakCompareAndSet rather than compareAndSet will introduce subtle and hard to reproduce synchronization errors into your code.
Ordering:
Let’s look example from first part:
public class BadlyOrdered {
boolean a = false;
boolean b = false;
void threadOne() {
a = true;
b = true;
}
boolean threadTwo() {
boolean r1 = b; // sees true
boolean r2 = a; // sees false
return r1 && !r2; // returns true
}
}
Compiler may reorder them. That is because compiler think it is not matter access which first. Because in single thread, they are ‘happen together’.
volatile
is tackles the visibility and ordering aspects. Let’s add volatile
.
public class BadlyOrdered {
volatile boolean a = false;
volatile boolean b = false;
void threadOne() {
a = true;
b = true;
}
boolean threadTwo() {
boolean r1 = b; // sees true
boolean r2 = a; // sees true
return r1 && !r2; // returns false
}
}
volatile
is guaranteeing what we save ordering for write in method threadOne()
.
Summarizing:
The volatile
keyword establishes an “happens before” relationship with written/read operations of a variable.
We must understand that using volatile does not solve synchronization problems.
When we declare a variable as volatile
, we lose performance, because the value is written/read immediately into memory, bypassing caches.
In this experiment, volatile writes are nearly 100x more expensive than normal writes.