preface

I haven’t updated this article for a long time (in fact, only three or four days), mainly because I was sick. I had a high fever and my body really couldn’t handle it, so I procrastinated. This is not, a little energy or immediately start to create. If there is wrong writing, but also hope that you see a lot of advice. Today I’m going to talk about volatile. Synchronized is a synchronized interview.

As we all know, volatile guarantees visibility and order, but not atomicity, which requires a locking mechanism like synchronized. So our understanding of volatile revolves around these three characteristics.

JMM

Before we learn about Volatile, it’s important to understand the JMM.

The JMM Java Memory Model, an abstract concept that doesn’t really exist, describes a set of rules or specifications that define how variables in a program can be accessed.

JMM rules on synchronization:

  1. The shared variable must be flushed back to main memory before the thread can be unlocked
  2. Before a thread locks, it must read the latest value of main memory into its working memory
  3. Lock unlocking is the same lock

Since the JVM runs programs in threads, the JVM creates a working memory for each thread when it is created. Working memory is the private data area for each thread, whereas the JMM specifies that all variables are stored in main memory, which is the shared memory area. All threads can access it, but thread operations on variables must take place in working memory. You first copy variables from main memory to your working memory space, then manipulate them, and then write them back to main memory. Variables in main memory cannot be manipulated directly. Working memory in each thread stores copies of variables in main memory. Therefore, different threads cannot access each other’s working memory, and communication between threads must be done through main memory.

Eight operations of the JMM

  • Read: Reads data from main memory
  • Load: Writes data read from main memory to working memory
  • Store: Writes data from working memory to main memory
  • Write: Assigns the store’s past variable value to a variable in main memory
  • Use: To compute by reading data from working memory
  • Assign: Reassigns the calculated value to the working memory
  • Lock: Locks the main memory variable, marking it as thread-exclusive
  • Unlock: Unlocks a main memory variable that can be locked by other threads

The consistency protocol does not lock the bus. As long as you set volatile, a lock instruction triggers cache invalidation

Through the flow chart, we can roughly analyze the execution flow of the two threads in the above code.

Thread 1:

  1. We’re going to read data in main memory.
  2. Load data into working memory
  3. The CPU uses the data

Thread 2: The first three steps remain unchanged. 4. Reassign the calculated value to the working memory 5. Write the working memory data to the main memory 6. Assigns the store’s past variable value to a variable in main memory

However, we can see that initFlag has been changed in the main thread, but thread 1 has not received the change signal, leaving it in the same state as before initFlag was changed.

So what if we want to implement inter-thread variable visibility?

visibility

Speaking of visibility, we have to mention the MESI cache consistency protocol.

MESI cache consistency protocol: Multiple main cpus read the same data into their respective caches. When one of the main cpus modifies the data in the cache, the data is immediately synchronized back to main memory. The other cpus use bus sniffing to sense the change and invalidate the data in their caches.

What about this bus sniffing mechanism? Each CPU has a listener on the bus. If one of our threads changes the value of a variable, its path back to main memory must go through the bus. So if another thread has been listening to the bus, when it finds that the value has changed, it will nullize the variable value in its working memory, and then go back to main memory to read the variable value.

Volatile Cache visibility implementation principle Assembly instruction LOCK

  1. The current processor cache row data is immediately written back to main memory
  2. This write operation triggers bus sniffing (MESI protocol)

This visibility is achieved by the underlying assembly’s Lock prefix instruction, which locks the cache of this area of memory (cache row locking) and writes it back to main memory. The working memory of other threads listens to the bus all the time. If this variable changes, the data cached by other cpus will be invalid (MESI protocol). To retrieve this value, you need to retrieve it from main memory again.

Atomicity is not guaranteed

So before we talk about not guaranteeing atomicity, let’s run a little code.

public class Test {
    public static volatile int num = 0;
    public static void increase(a) {
        num++;
    }
    public static void main(String[] args) throws InterruptedException {
        Thread[] threads = new Thread[10];
        for (int i = 0; i < threads.length; i++) {
            threads[i] = new Thread(new Runnable() {
                public void run(a) {
                    for (int j = 0; j < 1000; j++) { increase(); }}}); threads[i].start(); }for(Thread t : threads) { t.join(); } System.out.println(num); }}Copy the code

If a thread gives num + 1, the result will be written to the main thread. If other threads hear that num has changed, they will retrieve the value of num in main memory and increment it by one. But is that really the case? Let’s look at the results.

Sometimes it’s true. But if you run it a few more times, you’ll notice something’s still wrong.

Why is there less? This is the non-guaranteed atomicity of volatile. So let’s analyze why.

First thread 1 increments num by 1; And then write back to main memory. Because num is modified by volatile, other threads listen when num changes. Suppose that thread 1 increments num = 0 and thread 2 increments num +1. When thread 2 finds that num has changed, it will empty num in its working memory. Num = 1; So here’s the problem.

It’s worth two, but you’ve added it three times already. Three times is three, but you add three times is two. So that’s why it’s below 1000.

So if you want to guarantee atomicity, it’s easy, you sychronized num.

Instruction rearrangement

When a computer executes a program, to improve performance, the compiler and processor often reorder the instructions, generally divided into the following three types:

Source code — > rearrangement of compiler optimizations — > rearrangement of instructions parallel — > rearrangement of memory systems — > instructions finally executed

In a single-threaded environment, the final execution of the program is guaranteed to be consistent with the sequential execution of the code. However, in a multi-threaded environment, threads are executed alternately. Due to the existence of compiler optimization rearrangement, it is uncertain whether the variables used in the two threads can guarantee consistency, and the results are unpredictable. The processor must consider the data dependencies between instructions when reordering.

Volatile disables instruction reordering optimization to avoid out-of-order execution in multithreaded environments.

A Memory Barrier is a CPU instruction that does two things:

  1. Maintain the order in which certain operations are executed
  2. Keep some variables visible in memory

Because both the compiler and the processor can perform instruction rearrangement optimization. Inserting a Memory Barrier between instructions tells the compiler and CPU that no instructions can be reordered with the memory-barrier instructions. This prevents reordering optimizations for instructions before and after the Barrier by inserting a Barrier. Another function of the memory barrier is to force the cache data of various cpus to be flushed out, so that any thread on the CPU can read the latest version of the data.

To do this, the Java memory model adopts a strategy of inserting a StoreStore barrier before each volatile write. Insert a StoreLoad barrier after each volatile write. Insert a LoadLoad barrier after each volatile read. Insert a LoadStore barrier after each volatile read.

The write operation

A read operation

By using the barrier, we can make sure that other code does not interfere with the code inside the memory barrier.

conclusion

Today, I told you about volatile, which has three important points: visibility, order, and no guaranteed atomicity. The visibility is realized by the JMM model and the SNIFFING mechanism of MESI(Cache Consistency Protocol). Order is through the memory barrier, in the assembly level to ensure the order of the code before and after the barrier to ensure the order of the code to ensure the orderly execution; Finally, we analyze that volatile does not guarantee atomicity when the loop is similar to i++.