By Jacob Jankov

Original text: tutorials.jenkov.com/java-concur…

Translation: If you have a better translation version of Pan Shenlian’s personal website, please submit an issue or contribute to ❤️

Update: the 2022-02-24

Java’s volatile keyword is used to mark Java variables as “stored in main memory.” More precisely, each read of a volatile variable is read from the computer’s main memory, not from the CPU cache, and each write to a volatile variable is written to main memory, not just to the CPU cache.

In fact, since Java5, the volatile keyword has been used for more than just ensuring that volatile variables read and write to main memory. I will explain this in the following sections.

Java Volatile Tutorial video

If you like videos, I have a video version of the Java Volatile tutorial here: the Java Volatile Tutorial video

Variable visibility issues

The Volatile keyword in Java guarantees “visibility” of shared variables in multithreaded processing. This may sound abstract, so let me elaborate.

In multithreaded applications, if multiple threads operate on the same variable without declaring a volatile keyword, each thread can copy the variable from main memory to the CPU cache as it processes the variable, for performance reasons. If your computer has multiple cpus, each thread may run on a different CPU. This means that each thread can copy variables onto the CPU cache of a different CPU. This is illustrated here:

For variables that do not declare the volatile keyword, there is no guarantee that the Java Virtual Machine (JVM) will read data from main memory to the CPU cache or write data from the CPU cache to main memory. This can lead to several problems, which I’ll explain in the following sections.

Imagine a scenario where multiple threads access a shared object containing a counter variable that declares the following:

public class SharedObject {

    public int counter = 0;

}
Copy the code

Suppose that only thread 1 increments the counter variable, but thread 1 and thread 2 read the counter variable from time to time.

If the counter (counter) variable does not declare the volatile keyword, there is no guarantee that the value of the counter variable will be written from the CPU cache back to main memory. This means that the value of the counter variable on each CPU cache may differ from the value of the variable in main memory. The situation is as follows:

A thread’s write operation has not been written back to main memory (each thread has a local cache, known as the CPU cache, from which successful writes are flushed to main memory), and other threads cannot see the latest value of a variable. This is the “visibility” problem, where updates from one thread are not visible to other threads.

Java Volatile visibility guarantee

The Volatile keyword in Java is designed to solve the visibility problem. By declaring the volatile keyword on the counter variable, all thread writes to the variable are immediately synchronized to main memory, and all thread reads to the variable are read directly from main memory.

Here is the counter variable that declares the use of the keyword volatile:

public class SharedObject {

    public volatile int counter = 0;

}
Copy the code

Therefore, the variable with the volatile keyword is declared to ensure that it is visible to other threads writing to the variable.

In the above scenario, one thread (T1) changes the counter variable and another thread (T2) reads it (but does not change it). In this scenario, declaring the counter (counter) variable volatile This ensures that writes to the counter variable are visible to thread (T2).

However, if both threads (T1) and (T2) have made changes to counter (counter) variables, declaring the volatile keyword for counter (counter) variables does not guarantee visibility, as discussed later.

Volatile Global visibility guarantee

In fact, Java’s visibility guarantee for volatile keywords trumps that of volatile variables themselves, as follows:

  • If thread A writes A volatile variable and thread B subsequently reads the same volatile variable, the visibility of all variables is visible to thread A before thread A writes the volatile variable and to thread B after thread B reads the volatile variable.

  • If thread A reads A volatile variable, all variables visible to thread A are also reread from main memory when the volatile variable is read.

Let me illustrate with a code example:

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 udpate() method writes three variables, only of which days is declared volatile.

Variables declared by the volatile keyword are flushed directly from the local thread cache to main memory when written.

The global visibility guarantee for volatile means that when a value is written to days, all variables visible to the current writing thread are also written to main memory. When a value is written to days, the year and months variables are also written to main memory.

To read the values of years, months, and days, you can do this:

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

Note that the totalDays() method first reads the value of the days variable into the total variable, and when the program reads the days variable, it also reads the values of the month and years variables from main memory. So you can read the latest values of the three variables days,months, and years in the sequence above.

Instruction reordering challenges

To improve performance, the JVM and CPU are generally allowed to reorder instructions in a program without changing the semantics of the program. Such as:

int a = 1;
int b = 2;

a++;
b++;
Copy the code

These instructions can be reordered in the following order without losing the semantic meaning of the program:

int a = 1;
a++;

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

However, instruction reordering can be challenging when one of the variables is declared by the volatile keyword. Let’s look at an example of the MyClass class from the previous tutorial:

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

Once the update() method writes a value to the days variable, the most recent values written to the years and months variables are also written to main memory. However, if the Java virtual machine rearranges the instructions, for example:

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

When you modify the days variable, the values of the months and years variables are still written to main memory, but this occurs before the values are written to the months and years variables. So the latest values of the months and years variables cannot be correctly visible to other threads. This reordering of instructions results in semantic changes.

Java provides a solution to this problem, as we’ll see below.

Java volatile happens-before rule

To address instruction reordering challenges, Java’s volatile keyword provides happens-before rules in addition to visibility guarantees. Happens-before rules guarantee that:

  • If the read and write operations to other variables originally occurred involatileWrite operations on volatile variables must not be reordered after write operations on volatile variables.
    • involatileRead and write to other variables that occur Before a variable is written, happens-beforevolatileVariable write.

Note: For example, writes to other variables after volatile writes may still be rearranged before volatile writes. You just can’t do it the other way around, allowing subsequent reads and writes to be reordered to the front, but not allowing previous reads and writes to be reordered to the back.

  • If the read and write operations to other variables originally occurred involatileAfter a variable read, the reads and writes of other variables cannot be reordered before those of volatile variables.

Note: For example, reads of other variables before reads of volatile variables may be rearranged after reads of volatile variables. You just can’t do it the other way around, allowing previous reads to be reordered, but not subsequent reads to be reordered.

The happens-before rule above ensures that visibility guarantees for the volatile keyword are enforced.

Declaring volatile alone is not sufficient to ensure thread safety

Even though the volatile keyword guarantees that volatile variables are read directly from main memory, and all writes to volatile variables are written directly to main memory, declaring the variable volatile alone may not be thread-safe in some cases.

In the case explained earlier, only thread 1 writes the shared counter variable, and declaring the counter variable volatile is sufficient to ensure that thread 2 always sees the latest written value.

In fact, multiple threads can simultaneously write to a volatile shared variable and still store the correct value in main memory if the variable’s new value does not depend on the previous value. In other words, if a thread writes only to a volatile shared variable, it does not need to read the value of that variable and evaluate to the next value.

Once the thread needs to first read the value of a volatile variable and then generate new values for the volatile shared variable based on that value, volatile variables are no longer sufficient to ensure correct visibility. The short time between reading volatile variables and writing new values can result in resource contention, with multiple threads simultaneously reading volatile variables, getting the same values, assigning new values to the variables, and writing the values back to main memory, overwriting each other’s values.

The situation where multiple threads increment the same counter variable, causing volatile variables to be insufficient for thread safety. The following section explains the situation in more detail:

Imagine if thread 1 reads the shared counter variable (counter) with a value of 0 into its CPU cache, increments it to 1 and hasn’t written the changed value back to main memory. Thread 2 at the same time can also read the same counter variable from main memory, with a value of 0, into its own CPU cache. Thread 2 can then increment the counter to 1 without writing it back to main memory. This situation can be seen in the figure below:

Thread 1 and thread 2 are now almost out of sync. The actual value of the shared counter (counter) variable should be 2, but each thread has a variable value of 1 in its CPU cache, which is still 0 in main memory. What a mess! Even if the thread eventually writes the value of its shared counter variable back to main memory, the value will be incorrect.

When volatile is thread-safe

As I mentioned earlier, using the volatile keyword is not sufficient to keep threads safe if both threads are reading and writing shared variables. In general, you need to use synchronized to ensure that variable reads and writes are atomic. Reading or writing volatile variables does not block reading or writing by other threads. To do this, you must use the synchronized keyword around key sections.

As an alternative to synchronized blocks, you can choose to use java.util.concurrent and emit atomic data types in packages. For example, AtomicLong or AtomicReference or one of the others.

If only one thread reads and writes the value of a volatile variable, and the other threads read only the variable, the reader thread is guaranteed to see the last value written to the volatile variable. This is not guaranteed without making the variable volatile.

The volatile keyword is guaranteed to work on 32 – and 64-bit bits.

Performance considerations for Volatile

Reads and writes to volatile variables are written directly from main memory and are more expensive than reads and writes from the CPU cache, but accessing volatile variables prevents instruction reordering, which is a normal performance enhancement technique. Therefore, unless you really need to enforce visibility of variables, reduce the use of volatile variables in other cases.

(The end of this article)

Original text: tutorials.jenkov.com/java-concur…

Translation: If you have a better translation version of Pan Shenlian’s personal website, please submit an issue or contribute to ❤️