Java | Multithreading Part 2 : Atomicity, Visibility, Ordering

MrAndroid
5 min readDec 6, 2022

--

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.

Cached variable in Thread

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.

More details

--

--