The volatile keyword in Java is used to mark Java variables as “always stored in main memory.” This means that volatile variables are read from main memory each time, and that every change to a volatile variable is written back to main memory instead of the CPU cache.

In fact, since java5, the volitle keyword guarantees more than just volatile variables that are always read and modified in main memory.

Variable visibility issues

Volatile ensures that changes to shared variables are visible between threads.

In multithreaded applications, for variables that are not volatile, each thread makes a copy of the variable from main memory to the CPU cache during execution. Given that you have multiple cpus in your computer, each thread may execute on a different CPU, meaning that each thread is likely to load variables into a different CPU cache. As shown in the figure:

The absence of volatile decorations will not guarantee when a variable is read from or written back to main memory by the JVM. This leads to several problems.

Suppose two or more threads access a shared object containing the counter variable, declared as follows:

public class SharedObject{
    public int counter = 0;
}
Copy the code

Imagine that only thread 1 accumulates counter, but thread 1 and thread 2 will load the variable counter from main memory at some point in time.

There is no guarantee that changes to counter will be written back to main memory if they are not volatile. This means that the value of the counter variable in different CPU caches may not be the same as that in main memory. As shown in the figure:

The problem is that thread 1’s modification of the counter variable is impossible to see for thread 2. The problem of one thread making changes to a shared variable invisible to another thread is called the “visibility” problem.

Volatile’s guarantee of visibility

The volatile keyword in Java is used to solve visibility problems. If you make counter volatile, any changes to counter are immediately written back to main memory, limiting it to being read from main memory.

public class SharedObject{
    public volatile int counter = 0;
}
Copy the code

Modifying variables to volatile ensures that the changes are visible to other threads.

As mentioned above, thread 1’s changes to the counter can be volatile for thread 2 to ensure that thread 1’s changes to the counter variable are visible to thread 2.

However, if both threads 1 and 2 accumulate the counter variable, it is not sufficient to simply make the variable volatile. Details will be given below.

Volatile’s full guarantee of visibility

In fact,volatile’s protection against visibility is not limited to the variables themselves that volatile modifies. The contents of visibility guarantee are as follows:

  • If thread 1 changesvolatileModify the variable immediately after thread 2 reads the samevolatileModify the variable, then thread 1 on modificationvolatileChanges to other variables before the variable are visible to thread 2.
  • If thread 1 reads onevolatileModify the variable, thenvolatileAny other variables used after modifying a variable are forced to be read from main memory so that all variables are visible to thread 1.

Example code:

public class MyClass {
    private int years;
    private int months
    private volatile int days;


    public void update(int years, int months, int days){
        this.years  = years;
        this.months = months;
        this.days = days; }}Copy the code

The update() method is used to update three variables, of which only days is volatile.

The sufficient guarantee of visibility by volatile means that when a thread updates the value of days, it is written back to main memory, along with the yeas months updates that preceded the days.

When years months days is read:

public class MyClass {
    private int years;
    private int months
    private volatile int days;

    public int totalDays(a) {
        int total = this.days;
        total += months * 30;
        total += years * 365;
        return total;
    }

    public void update(int years, int months, int days){
        this.years  = years;
        this.months = months;
        this.days = days; }}Copy the code

Notice the totalDays() method, which initially assigns the value of days to total and then reads from main memory along with the months and years participating in the calculation. So you can ensure that the days months and years reads above are up to date.

Instruction reordering challenges

The JVM and CPU can reorder instructions in a program semantically for better execution. As follows:

int a = 1;
int b = 2;

a++;
b++;
Copy the code

These instructions can be reordered semantically:

int a = 1;
a++;

int b = 2;
b++;
Copy the code

However, there are challenges when reordering volatile modified variables.

Take a look at the example mentioned earlier:

public class MyClass {
    private int years;
    private int months
    private volatile int days;

    public int totalDays(a) {
        int total = this.days;
        total += months * 30;
        total += years * 365;
        return total;
    }

    public void update(int years, int months, int days){
        this.years  = years;
        this.months = months;
        this.days = days; }}Copy the code

Once the update() method updates the days variable, updates to years and months are also written back to main memory if the JVM resorts:

public void update(int years, int months, int days){
    this.days   = days;
    this.months = months;
    this.years  = years;
}
Copy the code

When changes are made to the days variable, the changes to months and years are also written back to main memory, but this time the changes to days are made before months and years, so the most recent changes to months and years are not visible to other threads. The semantics of the reorder have changed.

The protection of happens-before by volatile

In response to instruction reordering challenges,volatile provides a “happens-before” safeguard to supplement visibility protection. The happens-before safeguard is as follows:

  • The order in which other variables are read and written before volatile modifiers cannot be reordered to after. This ensures that reads and writes to other variables are normal before writing to volatile variables. In contrast, the order in which reads and writes to other variables are allowed is reordered after volatile modifiers are written.
  • Reads to other variables in the order in which they were read from volatile variables cannot be reordered to prior. Ensures that reads and writes to other variables after volatile readings occur normally. Instead, the order in which reads and writes to other variables were allowed before reading volatile modifiers is reordered to after.

Happens-before ensures that volatile visibility guarantees are enforced.

Volatile is not always sufficient

Although volatile guarantees that volatile variables are always read and written back from main memory, there are cases where volatile variables are not sufficient.

Previously, thread 1’s changes to volatile variables were always visible to thread 2.

In multithreading, if the new value generated does not depend on the old value in main memory (the old value does not need to be used to derive the new value), then both threads can update volatile variable values in main memory at the same time.

When new values generated by a thread depend on old values, it is not sufficient to use volatile to modify shared variables to ensure visibility. A race condition occurs when two threads load the same volatile variable from main memory, update it and write it back to main memory at the same time before a thread accesses the volatile shared variable. The updates from the two threads override each other. The value that the thread later reads from main memory may be wrong.

In this case, when two threads are accumulating the same counter variable at the same time, volatile modification of the variable is no longer sufficient. As follows:

When thread 1 loads counter from main memory into CPU cache, it will be 0 at this time, and it will be 1 after adding counter. At this time, thread 1 has not written counter back to main memory, and thread 2 will also load counter from main memory into CPU cache for adding operation. Thread 2 hasn’t written counter back to main memory yet.

Thread 1 and thread 2 actually go on at the same time. The expected result of the counter variable in main memory should be 2, but as shown, the two threads have a value of 1 in their caches and 0 in main memory. It is an error even if two threads write values from their respective caches back to main memory.

What is sufficient for volatile?

When two threads simultaneously read and write to a shared variable, the volatile modifier is no longer sufficient. You need to use synchronized to ensure atomicity of variable reads and writes. Using volatile does not synchronize thread reads and writes. In this case, only the synchronized keyword can be used to modify critical section code.

In addition to synchronized, you can select atomic data types provided in the java.util.concurrent package. For example, AtomicLong and AtomicRefrerence.

In other cases, volatile is sufficient to guarantee visibility if only one thread reads or writes volatile variables and the other threads only read them, but not if they are not volatile.

The volatile keyword is available on 32bit and 64 variables.

Volatile practice recommendations

Reads and writes to volatile modified variables can be forced from main memory (read from and written back to main memory). Reading and writing directly from main memory has a much higher performance cost than reading and writing from the CPU cache. Volatile can be effective in preventing instruction reordering in certain situations. Therefore, volatile should be used sparingly, only when there is a genuine need to protect variable visibility.

This post series is the result of a review of my basic translation or understanding of the Java Concurrency and Multithreading Tutorial by Jakob Jenkov

Previous article: Synchronizing code blocks Next: ThreadLocal