This article was first published on wechat official account Lao Hu Ma Zi

In this article, we will introduce the memory semantics of volatile and the memory barriers that implement its characteristics. In this article, we will introduce the memory semantics of volatile and the memory barriers that implement its characteristics.

Volatile is one of the lightest synchronization mechanisms provided by the JVM because the Java memory model defines special access rules for volatile, enabling it to implement two of the characteristics of the Java memory model: visibility and ordering. Because of these two properties, the volatile keyword can be used to solve some synchronization problems in multiple threads.

Visibility of volatile

The visibility of volatile means that when a variable is volatile, it is visible to all threads. In plain English, this means that when a thread changes a volatile variable, other threads can immediately learn about the change and get the latest value of the variable.

Combined with the previous article mentioned threads in the Java memory model, working memory, the interaction between main memory and our visibility of volatile can understand so, defined as volatile modified variable, in a thread to write without the value work cached in memory, but to refresh the modified value is directly back to main memory, When the processor detects a change in the main memory address of the variable in other threads, it will tell those threads to go back to main memory and copy the latest value of the variable into the working memory instead of continuing to use the old cache in the working memory.

Here is an example of using volatile visibility to address multithreaded concurrency security:

public class VolatileDemo {
    //private static boolean isReady = false;
    private static volatile boolean isReady = false;

    static class ReadyThread extends Thread {

        public void run(a) {
            while(! isReady) { } System.out.println("ReadyThread finish"); }}public static void main(String[] args) throws InterruptedException {
        new ReadyThread().start();
        Thread.sleep(1000);//sleep 1 second to ensure that the ReadyThread thread has started executing
        isReady = true; }}Copy the code

Running the code above will eventually print ReadyThread Finish on the console, and if you remove the volatile modifier isReady and run it again, you will find that the program continues without ending, and the console does not print anything.

Let’s analyze this program: IsReady is initially false, so once the ReadyThread starts executing, its while block goes into an infinite loop because the flag isReady is false. When isReady is volatile, After the main thread changes isReady to true, the ReadyThread immediately gets the latest isReady value, and the while loop ends, so the text is printed. When volatile is not used, the main thread changes the isReady variable, but the ReadyThread is unaware of the change and uses the same value, thus executing the while in an infinite loop.

The order of volatile

Orderliness means that program code is executed in the order in which it is implemented.

The ordering nature of volatile is to prohibit JVM instruction reordering optimizations.

Let’s look at an example:

public class Singleton {
    private static Singleton instance = null;
    //private static volatile Singleton instance = null;
    private Singleton(a) {}public static Singleton getInstance(a) {
        // First judgment
        if(instance == null) {
            synchronized (Singleton.class) {
                if(instance == null) {
                    // Initialize, not atomic operation
                    instance = newSingleton(); }}}returninstance; }}Copy the code

The above code is a very common implementation of the singleton pattern, but the above code is problematic in a multithreaded environment. Instance = new Singleton(); instance = new Singleton(); This initialization operation is not atomic and corresponds to the following instructions on the JVM:

memory =allocate(); //1. Allocate ctorInstance(memory); //2. Initialize the object instance =memory; //3. Set instance to the newly allocated memory addressCopy the code

In the above three instructions, Step 2 depends on Step 1, but Step 3 does not depend on Step 2. Therefore, THE JVM may conduct instruction rebeat optimization for them, and the rearranged instructions are as follows:

memory =allocate(); Instance =memory; // set instance to the newly allocated memory address ctorInstance(memory); //2. Initialize the objectCopy the code

After this optimization, the memory initialization is placed after the instance allocation, so that when thread 1 executes the assignment in step 3, another thread 2 enters the getInstance method to check that instance is not null. The instance that thread 2 gets is not initialized yet, so using it will cause an error.

So when we implement the singleton pattern in this way, we use the volatile keyword to decorate the instance variable, because the volatile keyword protects against instruction reordering in addition to ensuring visibility. When volatile is used, the JVM does not reorder the initialization instructions mentioned above, and thus does not have multithreaded safety issues.

Volatile usage Scenarios

Volatile can be used in the following situations:

  • The volatile keyword is only used when the result of the operation does not depend on the variable’s current value, or when we can ensure that only a single thread changes the variable’s value
  • Variables do not need to participate in invariant constraints with other state variables

Volatile and atomicity

Volatile guarantees visibility and order, but does not guarantee atomicity. Here is another example of volatile and atomicity:

public class VolatileTest {
    public static volatile int count = 0;

    public static void increase(a) {
        count++;
    }

    public static void main(String[] args) {
        Thread[] threads = new Thread[20];
        for(int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(() -> {
                for(int j = 0; j < 1000; j++) { increase(); }}); threads[i].start(); }// Wait for all accumulated threads to finish
        while (Thread.activeCount() > 1) { Thread.yield(); } System.out.println(count); }}Copy the code

This code creates 20 threads, each of which increments count 1000 times. If this code were concurrent, the result would be 20000, but it is often less than 20000 because count++ is not atomic.

The count++ increment operation above is equivalent to count=count+1, so the JVM needs to read the value of count, add one to it, and then reassign the new value to the count variable, so the increment takes three steps.

In the figure above, I draw a simple process for thread increment of count. When a thread wants to increment count, it first reads the value of count, then performs count+1 operation based on the current value of count, and finally writes the new value of count back to count.

If thread 2 is reading the value of count while thread 1 is reading the old value of count and writing back the new value of count, thread 2 is reading the old value of count that has not been updated, and the two threads are +1 on the same value, so the two threads are not adding count. However, these operations do not violate the definition of volatile, so the use of volatile can still be problematic for multithreaded concurrency.

Volatile and memory barriers

How does the JVM implement these two features for volatile? The Java Memory model is implemented through Memory barriers.

Memory barriers are also JVM instructions, and the reordering rules of the Java memory model require the Java compiler to insert specific memory barrier instructions when generating JVM instructions to prohibit the reordering of specific instructions.

In addition, the memory barrier has some semantics: all writes before the barrier are written back to the main memory, and all reads after the barrier get the latest results of all writes before the barrier (achieving visibility). Therefore, instructions behind the memory barrier are not allowed to be reordered before the memory barrier.

The following table is a list of actions that prohibit instruction reordering with respect to volatile:

The first operation Second operation: normal read and write The second operation: volatile read The second operation: volatile write
General speaking, reading and writing You can rearrange You can rearrange It cannot be rearranged
Volatile read It cannot be rearranged It cannot be rearranged It cannot be rearranged
Volatile write You can rearrange It cannot be rearranged It cannot be rearranged

From the table above, we can draw the following conclusions:

  1. When the second volatile operation writes, there is no reordering, no matter what the first operation is. This rule ensures that operations prior to volatile writes are not reordered after volatile writes.
  2. When the first operation is a volatile read, no matter what the second operation is, it cannot be rearranged. This operation ensures that operations following volatile reads are not reordered before volatile reads.
  3. If the first operation is volatile write and the second operation is volatile read, reordering cannot be performed.

There are four classes of memory barrier instructions available in the JVM:

Barrier type Order sample instructions
LoadLoad Load1; LoadLoad; Load2 Ensure that the read operation of load1 is executed before the read operation of load2 and subsequent reads
StoreStore Store1; StoreStore; Store2 Before performing write operations on Store2 and store1, ensure that the write operations on store1 have been flushed to the main memory
LoadStore Load1; LoadStore; Store2 Ensure that the read operation of load1 is complete before stroe2 and subsequent write operations
StoreLoad Store1; StoreLoad; Load2 Ensure that the write operations of store1 have been flushed to main memory before load2 and subsequent read operations can be executed

conclusion

Volatile implements visibility and orderliness in the Java memory model, both of which are implemented through memory barriers, and does not guarantee atomicity.