Concurrent programming: JMM

Time:2022-5-23

Hello, I’m Xiao Hei, a migrant worker who lives on the Internet.

In the last issue, I shared some basic knowledge about threads in Java. In the example of thread termination, the first method says that to terminate a thread, you can use the flag bit method. Let’s review the code again.

class MyRunnable implements Runnable {
    //Volatile keyword to ensure that the current thread can see the changed value (visibility) after the main thread is modified
    private volatile boolean exit = false; 
    @Override
    public void run() {
        While (! Exit) {// loop to judge whether the flag bit needs to be exited
            System. out. Println ("this is my custom thread");
        }
    }
    public void setExit(boolean exit) {
        this.exit = exit;
    }
}
public class ThreadDemo {
    public static void main(String[] args) {
        MyRunnable runnable = new MyRunnable();
        new Thread(runnable).start();
        runnable. setExit(true); // Modify the flag bit and exit the thread
    }
}

In this code, the flag bitexitField is used in the declarationvolatileThe purpose of shutdown word modification is to ensure that the current thread can perceive the change after another thread is modified. What does this keyword do? In this issue, let’s talk in detail.

Before we start talking about volatile keyword, we need to talk about the memory model of computer.

Memory model of computer

The so-called memory model is described in English as memory model, which is a relatively low-level thing. It is a concept related to computer hardware.

As we all know, when the computer executes the program, the instructions are finally executed in the CPU one by one, and there is often data transmission in the execution process. The data is stored in the main memory. Yes, it’s your memory module.

At the beginning, when the execution speed of the CPU is not fast enough, there is no problem, but with the continuous development of CPU technology, the CPU computing speed is getting faster and faster. However, the speed of reading and writing data from the main memory is a little slow, which leads to a lot of waiting time for the CPU to operate the main memory every time.

Technology always needs to move forward. We can’t stop the development of CPU because of the slow reading and writing of memory, and we can’t make the reading and writing speed of main memory a bottleneck.

It must also be thought of here, that is, add a cache between the CPU and the main memory, copy the required data on the cache, and the feature of the cache is that it reads and writes quickly, and then synchronize the data in the cache with the main memory regularly.

image

Is the problem solved here? Too young, too simple ah, this structure has no problem in the case of threading. With the continuous improvement of computer capacity, it begins to support multithreading, and the CPU is forced to support multi-core. Up to the current 4-core, 8-core and 16 core, there will be some problems in this case. Let’s analyze it.

Single core multithreading:Multiple threads access a shared data at the same time. The CPU loads the data from the main memory into the cache. Multiple threads will access the same address in the cache. In this way, the cached data will not become invalid even when the threads are switched. Because only one thread can execute at the same time in a single core CPU, there will be no data access conflict.

Multi core multithreading:Each CPU core will copy a copy of data to its own cache. In this way, the two threads on different cores are parallel, which will lead to inconsistent data cached by the two cores. This problem is calledCache consistency problem

image

In addition to the cache consistency problem mentioned above, in order to make full use of the computing power of the CPU, the computer will disorderly process the input instructions, which is calledProcessor optimization。 In order to improve the execution efficiency, many programming languages will also reorder the execution order of the code. For example, the just in time compiler (JIT) of our Java virtual machine will also do this. This action is calledInstruction rearrangement

int a = 1;
int b = 2;
int c = a + b;
int d = a - b;

For example, when we write this code, the execution order of the third and fourth lines may change, which is no problem in a single thread, but in the case of multithreading, it will produce different results than we expected.

In fact, the cache consistency problems mentioned above, processor optimization and instruction rearrangement correspond to the problems in our concurrent programmingVisibility problem, atomicity problem, ordering problem。 With these problems, let’s take a look at how to solve them in Java.

Because of these problems, there must be a mechanism to solve them. The mechanism for this solution isMemory model

The memory model defines a specification to ensure the visibility, ordering and atomicity of shared memory. How is the memory model solved? There are two main ways:Restricted processor optimizationandMemory barrier。 Let’s not delve into the underlying principles here.

JMM

From the above, we know that the memory model is a specification used to solve some problems in the case of concurrency. Different programming languages have corresponding implementations for this specification. Then JMM (JAVA memory model) is the concrete implementation of this specification in Java language.

So how does JMM solve this problem? Let’s look at the following picture first.

image

Memory visibility issues

Let’s look at the problem one by one. First, how to solve itVisibility issues

As shown in the above figure, in JMM, the operation of a thread on a data is divided into six steps.

They are: read, load, use, assign, write, store

If the volatile keyword is not used in the declaration of this variable, then the two threads copy a copy to the working memory respectively, thread B assigns the flag to true, and thread a is invisible.

To make thread a visible, you need to add the volatile keyword when declaring the variable flag. So what does JMM do after adding keywords? There are two situations: reading and writing.

  1. When a thread reads a volatile variable, JMM will set the variable in the working memory as invalid and read it from the main memory again;
  2. When a thread writes a volatile variable, it will immediately refresh the value in working memory to main memory.

In other words, for variables modified by volatile keyword, read, load and use operations must be performed together; Assign, write and store operations are executed together.

In this way, the problem of memory visibility can be solved.

Instruction rearrangement

For the problem of instruction rearrangement, the compiler will not optimize the instruction rearrangement as long as the object is declared volatile.

The volatile rule that prohibits instruction rearrangement complies with a rule called happens before.

Happens before has some other rules besides the volatile variable rule.

Procedure sequence rules:In a thread, the execution result of a piece of code is orderly. It will also rearrange instructions, but whatever it is, the result will be generated in the order of our code and will not change.

Tube side locking rules:Whether in a single thread environment or a multi-threaded environment, for the same lock, after one thread unlocks the lock, another thread obtains the lock and can see the operation result of the previous thread! (pipe pass is a general synchronization primitive, and synchronized is the implementation of pipe pass)

Volatile variable rule:That is, if a thread writes a volatile variable first, and then a thread reads the variable, the result of the write operation must be visible to the reading thread.

Thread start rule:When main thread a starts child thread B during execution, the modification result of shared variables made by thread a before starting child thread B is visible to thread B.

Thread termination rule:During the execution of main thread a, if sub thread B terminates, the modification results of shared variables made by thread B before termination are visible in thread a. Threads are also called join rules.

Thread interrupt rule: the call to the thread interrupt () method occurs first when the interrupted thread code detects the occurrence of an interrupt event. You can use thread Interrupted() detected whether an interrupt occurred.

Transitivity rules:The happens before principle is transitive, that is, HB (a, b), HB (B, c), then HB (a, c).

Object termination rule:The completion of the initialization of an object, that is, the end of the constructor execution, must be preceded by its finalize () method.

Race condition

Here, do you feel that the problem has been solved? Emmm, let’s look at the following scenario:

image

Suppose that thread a and thread B in the above figure are executed on two CPU cores in parallel. They read that the value of I is equal to 0, then add 1 respectively, and then write to the main memory together. If thread a and thread B execute in sequence, the value of I should be equal to 2 at last, but in parallel, it is possible to operate at the same time, and the value written back to the main memory is only increased once.

It’s like your bank card receives two transfers of $100, but there is only $100 more in your account.

This problem cannot be solved through volatile, which will not guarantee the atomicity of the variable operation. How can we solve this problem? We need to use synchronized to lock this operation to ensure that only one thread can operate at the same time.

summary

Because there is a cache between CPU and memory, there may be a cache consistency problem in the case of multithreading concurrency; The CPU will do some processor optimization for the input instructions, and some high-level language compilers will also do instruction rearrangement. Because of these problems, we will have memory visibility problems and ordering problems in the case of concurrency, and JMM appears in Java to solve these problems. Volatile keyword ensures memory visibility and prevents instruction rearrangement. However, volatile can only guarantee the order of operations, but can not guarantee the atomicity of operations. Therefore, for safety, we should lock the concurrent processing of shared variables.


OK, that’s all for today. I’ll see you next time.
Follow official account【Xiao Hei said Java】Get more dry goods.