2
头图
Author: Jacob Jankov
Original: http://tutorials.jenkov.com/java-concurrency/volatile.html
Translation: Pan Shenlian's personal website If you have a better translated version, welcome ❤️ Submit issue or contribute~
Updated: 2022-02-24

Java's volatile keyword is used to mark Java variables as "stored in main memory". More precisely, every read to the volatile variable will be read from the computer's main memory, not from the CPU cache, and every write to the volatile variable will be written to main memory, not only Write only in CPU cache.

In fact, since Java 5, the volatile keyword is not only used to ensure that volatile variable reads and writes main memory. I will explain this below.

Java volatile tutorial video

If you like videos, I have a video version of this Java volatile tutorial here:
Java volatile tutorial video

Variable visibility issues

Java's volatile keyword guarantees "visibility" of shared variables in multithreading. This might sound a little abstract, so let me elaborate.

In a multithreaded application, if multiple threads operate on the same variable without the declared volatile keyword, for performance reasons, each thread can copy the variable from main memory to the CPU cache while processing the variable. If your computer has multiple CPUs, each thread may run on a different CPU. This means that each thread can copy variables on the CPU caches of different CPUs. This is explained here:

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

Imagine a scenario where multiple threads access a shared object that contains a counter variable declared as follows:

public class SharedObject {

    public int counter = 0;

}

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

If the counter variable does not declare the volatile keyword, there is no guarantee when the value of the counter variable will be written back from the CPU cache to main memory. This means that the value of the counter variable on each CPU cache may not match the value of the variable in main memory. This situation looks like this:

The write operation of a thread has not been written back to the main memory (each thread has a local cache, that is, the CPU cache. Generally, if the write is successful, it will be flushed from the CPU cache to the main memory), and other threads cannot see the latest value of the variable, which is The "visibility" problem, where one thread's updates are not visible to other threads.

Java volatile visibility guarantee

Java's volatile keyword is to solve the variable visibility problem. By declaring volatile keyword on the counter variable, all threads' writes to this variable are synchronized to main memory immediately, and all threads' reads to this variable are directly read from main memory.

The following is the use of the counter variable declared with the keyword volatile :

public class SharedObject {

    public volatile int counter = 0;

}

Therefore, a variable with the keyword volatile is declared, which guarantees the visibility of writes to this variable by other threads.

In the scenario given above, one thread (T1) modifies the counter variable, and another thread (T2) reads the counter variable (but does not modify it), in this scenario, if the counter variable declares volatile key word, the write to the counter variable is guaranteed to be visible to the thread (T2).

But if both thread (T1) and thread (T2) have modified the counter (counter) variable, then declaring volatile keyword for the counter (counter) variable does not guarantee visibility, as discussed later.

volatile global visibility guarantee

In fact, Java's volatile keyword visibility guarantee exceeds the visibility of the volatile variable itself, and the visibility guarantees are as follows:

  • If thread A writes a volatile variable and thread B subsequently reads the same volatile variable, then the visibility of all variables, visible to thread A before thread A writes the volatile variable, and to thread B after thread B reads volatile variable Thread B is also visible.
  • If thread A reads a volatile variable, then when reading the volatile variable, all variables visible to thread A are also re-read from main memory.

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;
    }
}

udpate() method writes three variables, of which only the variable days is declared as volatile .

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

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

When reading the values of years , months and days , you can do:

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

    public int totalDays() {
        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;
    }
}

Note that the totalDays() method will first read the value of the days variable into the total variable. When the program reads days variable, it will also read the value of the month variable and years variable from the main memory. Therefore, you can guarantee to read the latest values of the three variables days , months and years through the above reading sequence.

The challenge of instruction reordering

In order to improve performance, the JVM and CPU are generally allowed to reorder the instructions in the program while keeping the program semantics unchanged. E.g:

int a = 1;
int b = 2;

a++;
b++;

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

int a = 1;
a++;

int b = 2;
b++;

However, instruction reordering presents some challenges when one of the variables is a variable declared with the volatile keyword. Let's look at the MyClass class example 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;
    }
}

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, such as this:

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

When the days variable is modified, the values of the years months are still written to main memory, but this node occurs before the new values are written to months and years variables. So the latest value of the months variable and the years variable cannot be properly visible to other threads. This rearrangement of instructions results in a change in semantics.

Java provides a solution to this problem, let's look down.

Java volatile Happens-Before rule

To address the challenge of instruction reordering, Java's volatile keyword provides Happens-Before rules in addition to visibility guarantees. The Happens-Before rule guarantees:

  • If the read and write operations of other variables originally occurred before the write operation of the volatile variable, then the read and write instructions of other variables cannot be reordered after the write instruction of the volatile variable;

    • Before the volatile variable is written, the reading and writing of other variables occurs, Happens-Before the writing of the volatile variable.
Note: For example, other variable reads and writes after the volatile variable is written may still be rearranged before the volatile variable is written. It's just not the other way around, allowing the following reads and writes to be rearranged to the front, but not allowing the previous reads and writes to be rearranged to the back.
  • If the read and write operations of other variables originally occurred after the read operation of the volatile variable, then the read and write instructions of other variables cannot be reordered before the read instructions of the volatile variable;
Note: other variable reads, such as volatile variable read, may be rearranged after the volatile variable read. It's just that it can't be reversed, allowing the previous reads to be rearranged to the back, but the latter reads are not allowed to be rearranged to the front.

The above Happens-Before rule ensures that the visibility guarantee of the volatile keyword is enforced.

Just declaring volatile is not enough to guarantee thread safety

Even though volatile keyword guarantees that the volatile variable is read directly from main memory, and all writes to the volatile variable are written directly to main memory, in some cases just declaring the variable volatile is not enough to guarantee thread safety.

In the case explained earlier, where only thread 1 writes to the shared counter variable, declaring the counter variable volatile is enough 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 new value written to the variable does not depend on the previous value. In other words, if a thread only writes to a volatile shared variable, it does not need to read the value of this variable first, and then calculate the next value.

Once the thread needs to first read the value of the volatile variable, and then generate a new value based on that value in the volatile shared variable, the volatile variable is no longer sufficient to guarantee correct visibility. The short time between reading the volatile variable and writing the new value creates resource contention, there are multiple threads reading volatile variable at the same time and getting the same value, all assigning the new value to the variable, and then writing the value back in main memory, thereby overwriting each other's values.

When multiple threads increment the same counter variable, the volatile variable is not enough to guarantee thread safety. The following sections explain this situation in more detail:

Imagine if thread 1 reads a shared counter variable with a value of 0 into its CPU cache, increments it to 1 and hasn't written the changed value back to main memory. At the same time thread 2 can also read the same counter variable from main memory, where the value of the variable is still 0 and stored in its own CPU cache. Then, thread 2 can also increment the counter to 1, without writing it back to main memory. This situation is shown in the following figure:

Thread 1 and Thread 2 are now almost out of sync. The actual value of the shared counter variable should be 2, but each thread has a variable value of 1 in its CPU cache and 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 wrong.

When is volatile thread safe

As I mentioned earlier, using the volatile keyword is not enough to guarantee thread safety if both threads are reading and writing to a shared variable. In this case, you need to use synchronized to ensure that variable reads and writes are atomic. Reading or writing volatile variable does not block other threads from reading or writing. To do this, you have to use the synchronized keyword around the critical section.

As an alternative to the synchronized block, you can choose to use the atomic data type from the java.util.concurrent package. For example, AtomicLong or AtomicReference or one of the others.

If only one thread reads and writes the value of the volatile variable, and the other threads only read the variable, then the reading thread is guaranteed to see the latest value written to the volatile variable. There is no guarantee without making the variable volatile .

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

Performance considerations for volatile

Reading and writing volatile variable will both read and write directly from main memory, which is more expensive than reading and writing from the CPU cache, but accessing the volatile variable prevents instruction reordering, which is a normal performance enhancement technique. So unless you really need to enforce the visibility of the variable, otherwise use the volatile variable less.

(End of this article)

Original: http://tutorials.jenkov.com/java-concurrency/volatile.html
Translation: Pan Shenlian's personal website If you have a better translated version, welcome ❤️ Submit issue or contribute~

潘潘和他的朋友们
94 声望110 粉丝