The volatile keyword of java concurrency

Hello everyone, I am Uncle San, I am very glad to meet you again in this issue, a worker who is struggling in the Internet. Today I will share with you a common keyword in java: volatile

What is volatile?

Volatile is a lightweight synchronization mechanism provided by the Java virtual machine, which ensures the visibility between threads sharing data and prohibits instruction reordering. When it comes to volatile, you have to say what is the java memory model (JMM)?

What is JMM?

In essence, JMM can be understood as the Java memory model that specifies how the JVM provides methods for disabling caching and compiling optimizations on demand. Communication between Java threads is governed by the Java Memory Model, and the JMM determines when a write to a shared variable by one thread is visible to another thread. JMM defines the abstract relationship between threads and main memory: shared variables between threads are stored in main memory (main memory), each thread has a private local memory (local memory), and the local memory stores the Threads to read/write copies of shared variables. Local memory is an abstraction of the JMM and does not really exist. It covers caches, write buffers, registers, and other hardware and compiler optimizations.

Shared variables between threads exist in main memory

Local memory A and B have a copy of the shared variable x in main memory. Assume that initially, the x values in all three memories are 0. When thread A executes, it temporarily stores the updated value of x (assumed to be 1) in its own local memory A.

When thread A and thread B need to communicate, thread A will first refresh the modified x value in its local memory to the main memory, and the x value in the main memory becomes 1 at this time. Subsequently, thread B goes to the main memory to read the updated x value of thread A, and at this time, the x value of thread B’s local memory also becomes 1.
The JMM provides memory visibility guarantees to Java programmers by controlling the interaction between main memory and each thread’s local memory.

In order to ensure memory visibility, the java compiler inserts memory barrier instructions at appropriate places in the generated instruction sequence to prohibit certain types of processor reordering.

Starting from JDK5, java uses the new JSR-133 memory model to propose the concept of happens-before, through which the memory visibility between operations is explained. If the result of an operation execution needs to be visible to another operation, there must be a happens-before relationship between the two operations. The two operations mentioned here can be within one thread or between different threads.

Having a happens-before relationship between two operations does not mean that the former operation must be executed before the latter operation! happens-before only requires that the previous operation (the result of the execution) be visible to the latter operation, and that the former operation be ordered before the second operation.

JMM is actually following a basic principle: as long as the execution result of the program does not change (referring to single-threaded programs and correctly synchronized multi-threaded programs), the compiler and processor can optimize whatever they want. For example, if the compiler, after careful analysis, determines that a lock will only be accessed by a single thread, then the lock can be eliminated. For another example, if the compiler determines after careful analysis that a volatile variable is only accessed by a single thread, then the compiler can treat the volatile variable as an ordinary variable. These optimizations will not change the execution result of the program, but can also improve the execution efficiency of the program.

How does java solve concurrency problems?

Understanding from another dimension: atomicity, visibility, orderliness

  1. Atomicity: In Java, the reading and assignment operations of variables of basic data types are atomic operations, that is, these operations cannot be interrupted, either executed or not executed. Only simple reading and assignment (and must assign a number to a variable, mutual assignment between variables is not an atomic operation) are atomic operations.
  2. Visibility: The Java memory model only guarantees that basic reading and assignment are atomic operations. If you want to achieve the atomicity of a wider range of operations, you can use synchronized and Lock to achieve it. Since synchronized and Lock can guarantee that only one thread executes the code block at any time, there is naturally no atomicity problem, thus ensuring atomicity.
    volatile ensures visibility. When a shared variable is modified by volatile, it will ensure that the modified value will be updated to the main memory immediately. When other threads need to read , it goes to memory to read the new value. In addition, visibility can also be guaranteed through synchronized and Lock. Synchronized and Lock can ensure that only one thread acquires the lock at the same time and then executes the synchronization code, and the modification of the variable will be refreshed to the main memory before the lock is released. So visibility is guaranteed.
  3. Orderliness: Locking ensures that only one thread executes at a time. Of course, there is orderliness. At the same time, JMM provides a happens-before rule to ensure orderliness.

How does volatile guarantee instruction rearrangement?

Memory barriers (Memory Barries) are inserted before and after the instruction sequence generated by volatile to prohibit processor reordering.
There are four types of memory barriers for volatile. The following picture is from Baidu Encyclopedia:

How does volatile guarantee write order?

Insert a StoreStore barrier (write-write barrier) in front of a volatile write operation and a StoreLoad barrier (write-read barrier) after a volatile write operation

How does volatile guarantee read order?

Insert a LoadLoad barrier (read-read barrier) after a volatile read operation, and insert a LoadStore barrier (read-write barrier) after a volatile read operation

Here comes the key point, how does volatile guarantee visibility?

I wrote a case: why the calculation result is not equal to 1000, I checked the information on the Internet, it turns out that the shared variables between threads cannot be seen in time!

public class demoController {<!-- -->

    public static void main(String[] args) throws InterruptedException {<!-- -->
        final int threadSize = 1000;
        ThreadUnsafeExample example = new ThreadUnsafeExample();
        // CountDownLatch: A thread (or multiple), wait for other N threads to complete something before executing.
        final CountDownLatch countDownLatch = new CountDownLatch(threadSize);
        // create thread pool
        ExecutorService executorService = Executors. newCachedThreadPool();
        // After Java5, it is better to start threads through Executor than Thread's start().
        for (int i = 0; i <threadSize; i ++ ) {<!-- -->
            executorService. execute(() -> {<!-- -->
                example. add();
                // Decrease the number of threads by one
                countDownLatch. countDown();
            });
        }
        countDownLatch. await();
        countDownLatch. countDown();
        System.out.println(example.get());
    }
    // Why is the execution result not equal to 1000?
    // If thread 1 is executed by CPU1, thread 2 is executed by CPU2. From the above analysis, it can be seen that when thread 1 executes the sentence i = 10, it will first load the initial value of i into the cache of CPU1, and then assign the value to 10.
    // Then the value of i in the cache of CPU1 becomes 10, but it is not written into the main memory immediately. At this time, thread 2 executes j = i, it will first go to the main memory to read the value of i and load it into the cache of CPU2,
    // Note that the value of i in the memory is still 0 at this time, then the value of j will be made 0 instead of 10.
    // This is the visibility problem. After thread 1 modifies the variable i, thread 2 does not immediately see the value modified by thread 1
}

Why is the execution result not equal to 1000?

Or return to three points: atomicity, visibility, and orderliness!

  1. Atomicity: caused by cpu time-sharing multiplexing, (thread switching)
    First of all, we must understand which steps an object has gone through?
    for example:
int i = 1;
i++;

This performs a three-step operation:
1. The value of i is read from the memory into the cpu register
2. The value of i is carried + 1,
3. Write the new value from the register to the memory (the cache mechanism may cause the CPU cache to be written instead of the memory)
In a single-threaded environment, these three steps are atomic, and there is no multi-threaded competition problem, but in a multi-threaded environment, these three steps are not performed in this order.
When thread 1 performs the first step, the cpu switches to thread 2 and performs three steps. At this time, if thread 1 finishes executing, due to the existence of CPU time-sharing multiplexing (thread switching), thread 1 executes After executing the first instruction, switch to thread 2 for execution. If thread 2 executes these three instructions, then switch to thread 1 to execute the next two instructions, which will cause the last i value written to the memory to be 2 instead of 3. .
So it is not difficult to understand why the value calculated above is not equal to 1000.

  1. Orderliness: Caused by reordering —– these reorderings may cause memory visibility problems in multi-threaded programs a->c b->c can be written as a-》b->c b-a->c Here only It is necessary that c be before ab, and the compiler and processor can reorder them. Here A happens-before B, but in actual execution, B can be executed before A (see the execution order after reordering above).
    As mentioned above, if A happens-before B, JMM does not require that A must be executed before B. The JMM only requires that the previous operation (the result of the execution) be visible to the subsequent operation, and that the previous operation precedes the second operation in order. Here, the execution result of operation A does not need to be visible to operation B; and the execution result after reordering operation A and operation B is consistent with the execution result of operation A and operation B in the happens-before order. In this case, JMM will think that this reordering is not illegal (not illegal), and JMM allows this reordering.
    JMM’s processor reordering rules require the java compiler to insert specific types of memory barrier instructions when generating instruction sequences, and prohibit specific types of processor reordering through memory barrier instructions.
    So when the JVM actually executes this code, will it guarantee that statement 1 will be executed before statement 2? Not necessarily, why? Instruction reordering may occur here. Commands will be reordered.
  2. Visibility: CPU cache caused! The value of i in the cache of CPU1 has changed to 10, but it is not written into the main memory immediately. At this time, thread 2 executes j = i, it will first go to the main memory to read the value of i and load it into the cache of CPU2, note that the value of i in the memory is still 0 at this time, then the value of j will be made 0, and Not 10.

So how to solve it?

  1. Use volatile to ensure cache visibility
  2. At the same time, the method is locked (synchronized) to ensure atomicity and order

Written at the end

This article introduces from the bottom of volatile why volatile can guarantee visibility and reordering, and has a deeper understanding of the volatile keyword.

Tips

good night~