The novice thoroughly understood the volatile keyword!

If you are beginning to learn about Concurrency in Java, you will find that the volatile keyword is volatile.

  • Ensure visibility of variables
  • Disallow instruction reordering

But what is visibility? How do you guarantee that? How do I disable instruction reordering? What’s the good of that? Xiao Qi has a big doubt, so immediately began a big search, finally understand it!

First, the brain map:

Guaranteed visibility

What is memory visibility?

This refers to the visibility of shared variables between threads. When one thread modifies a shared variable, another thread can read the modified value.

Last time, let’s look at a piece of code to see if this program terminates properly.

public class Demo {

    private  static boolean  flag = false;

    public static void main(String[] args) throws InterruptedException {
        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"Performing");
            while(! flag){ } System.out.println(Thread.currentThread().getName()+"End of execution");

        },"A").start();

        TimeUnit.SECONDS.sleep(2);

        new Thread(()->{
            System.out.println(Thread.currentThread().getName()+"Performing");
            flag = true;
            System.out.println(Thread.currentThread().getName()+"Changed the flag");
        },"B").start(); }}Copy the code

The result is that the sentence “execution of A ends” will not be entered and the program cannot be terminated.

If flag is not volatile, thread A sleeps for 2 seconds before thread B starts. That is, thread A brushes flag from main memory back to working memory before thread B makes changes to flag. The subsequent operations of thread B on flag are invisible to thread A, so the flag in thread A is always false and cannot jump out of the loop.

There are two ways to solve this phenomenon:

  • Will TimeUnit. SECONDS. Sleep (2); Commented out. (But this sometimes doesn’t work.)

    It is necessary to ensure that thread B modifies the flag and flusher it back to the main memory before thread A judges the flag. Thread A reads the flag for the first time, accesses the modified value in the main memory and places it in its own local memory. The program exits normally.

  • Add the volatile keyword to flag

    Regardless of whether there is A sleep operation, thread B can immediately sense the change of flag, and thread A can get the latest value. (This is the visibility of memory)

In many cases, our thread is moving at an unpredictable speed. With the first method, there is no guarantee that thread A has read A variable that has been modified by thread B and the program will exit normally. Using volatile ensures visibility between threads.

How do YOU achieve memory visibility?

This is not enough about the magic of volatile. Learning knowledge is not only about knowing what it is, but also about knowing why it is – how to achieve memory visibility? This brings us to the Java memory model and the special semantics of volatile! After reading these two concepts, we can understand ~

Java memory model

Because modern computers tend to store shared variables in high-speed buffers for efficiency, the main memory is in memory, which is relatively slow to access. Local memory is an abstraction, usually in caches, write caches, and registers.

Communication between Java threads is controlled by the JMM, which defines an abstract relationship between threads and main memory.

According to the JMM, all operations on shared variables must be performed by a thread in its own local memory and cannot be read directly from main memory.

As can be seen from the above model figure, thread B does not go to main memory to read the value of the shared variable. Instead, thread B finds it in local memory, finds that it has been updated, and then goes to main memory to read the new value of the shared variable, copies it to local memory, and then reads the new value of local memory.

For main memory and local memory interaction protocols, the Java memory model defines eight atomic operations to accomplish:

  • Lock: Acts on main memory, marking variables as thread-exclusive.
  • Unlock: Activates the main memory to unlock it.
  • Read: Uses main memory to transfer the value of a variable from main memory to the thread’s working memory.
  • Load: Applies to the working memory, putting the variable values passed by the read operation into a copy of the variable in the working memory.
  • Use: uses working memory to pass the value of a variable in the working memory to the execution engine.
  • Assign: assigns a value received from the execution engine to a variable in the working memory.
  • Store: variable applied to working memory that transfers the value of a variable in working memory to main memory.
  • Write: a variable that operates on main memory and places the value of the variable passed from the store operation into the main memory variable.


Volatile memory semantics for write-read

Let’s still take the chestnuts above and draw the JMM model:

How does a thread discover that a variable modified by volatile has been updated? This is due to the special semantics of volatile.

  • Write memory semantics: When a volatile flag variable is written, the JMM flusher the local memory variable corresponding to that thread to main memory. That is: assign -> store->write action must appear consecutively.

  • Read memory semantics: When a volatile flag variable is read, the JMM invalidates the thread’s local memory and reads the variable from main memory. That is, the read->load->use action must appear consecutively.

JMM memory barrier

At the bottom, the JMM protects the special semantics of volatile by preventing instruction reordering through memory barriers. This will be covered in more detail in the next section

Disallow instruction reordering

Why are instructions reordered?

Simply put, it is to optimize the program performance, the original instructions to execute the order of rearrangement.

There are three types of instruction rearrangements:

  • Compiler optimization rearrangement
    • The execution order of statements can be rearranged without changing the semantics of a single-threaded program.
  • Instruction parallel rearrangement
    • Modern processors use instruction-level parallelism to superimpose multiple instructions if there is no data dependency
  • The memory system is rearranged

Instruction reordering guarantees serial semantics, but there is no obligation to guarantee semantics consistency across multiple threads. Therefore, in multithreading, instruction reordering can cause problems.

JMM implementation of disallow instruction reordering

digression

In the old Java memory model, although reordering between volatile variables was prohibited, reordering of reads and writes between volatile variables and ordinary variables was still the same as we saw earlier, except that the objects were ordinary variables.

Therefore, the jSR-133 expert group decided to enhance the memory semantics of volatile by severely limiting compiler and processor reordering of volatile and common variables.

When the bytecode is generated, the compiler inserts a memory barrier into the sequence of instructions to prevent reordering of a particular type of handler. JMM uses a relatively conservative memory barrier insertion strategy:

  • Insert one before each volatile writeStoreStorebarrier
  • Insert one after each volatile writeStoreLoadbarrier
  • Insert one after each volatile readLoadLoadbarrier
  • Insert one more after each volatile readLoadStorebarrier

StoreStore barrier: Ensures that prior common writes are visible to any processor before volatile writes.

The StoreLoad barrier: Prevents volatile writes and possible volatile write/read reordering because the compiler cannot determine whether volatile reads/writes are following and conservatively inserts this barrier later

LoadLoad barrier: Prevents volatile reads and subsequent plain read reordering

LoadStore barrier: Avoids volatile reads followed by plain write reordering

Benefits of disallowing reordering?

Just for visibility. For example, the use of volatile in the DCL singleton pattern:

public class Singleton{
    private volatile static Singleton instance;
    
    public static Singleton getInstance(a){
        if(instance == null) {synchronized(Singleton.class){
                if(instance == null){
                    instance = newSingleton(); }}}returninstance; }}Copy the code
instance = new Singleton();
// Can be broken down into three steps
1. memory = allocate(); // Allocate memory
2. ctorInstanc(memory); // Initialize the object
3. s = memory; // Set s to the memory just allocated

// May be reordered to 1 -> 3 -> 2
Copy the code

If the volatile keyword is not used, for example, if thread A has performed 1-> 3, then thread B performs the first judgment, which determines that instance is not null, and returns an uninitialized instance.

With the volatile keyword, the JMM uses the StoreLoad memory barrier to avoid volatile writes and subsequent volatile read reordering.

Note that volatile does not prevent the 1->2->3 instruction reordering inside instance = new Singleton(), but guarantees that the read operation if(instance == null) will not be called until the write operation is complete.

withsynchronizedThe difference between

  • Performance: Volatile is a lighter thread to thread communication mechanism than locking, so it certainly performs better than synchronized.

  • Atomicity: Volatile guarantees atomicity only for reads and writes to individual volatile variables, whereas synchronized guarantees atomicity for the execution of entire critical sections of code.

  • Emphasis: Volatile is primarily used to address visibility of variables across multiple threads, while synchronized is more concerned with synchronization of access to resources between threads.

What is sniffing?

Sniffing is used to check for data invalidation, and is closely related to volatile.

When we talked about memory visibility, we raised the question: how does a thread find that a variable that is volatile has been updated? At the logical level, this is due to the special semantics of volatile. On the physical layer, each processor by sniffing the spread of the data on the bus to check the value of the cache is expired, when the processor found himself cache line corresponding to the memory address has been changed, and will set the current processing cache line to invalid state, when the processor to modify the data operation, Data is re-read from system memory into the processor’s cache.

Recommendations for using volatile

Because of the need for constant sniffing from main memory and CAS cycles to flush cached values back to main memory, invalid interactions can lead to spikes in bus bandwidth. So don’t use volatile in large numbers.

The resources

<< In-depth Understanding of the Java Virtual Machine >>

<< The Art of Concurrent Programming in Java >>

www.jianshu.com/p/8a2cf0c8c…