Interpretation of the volatile keyword of multithreading

   Friends who have been in contact with java know that java has many useful keywords, which are often used when we write code by ourselves, but few people really understand the meaning and implementation principle. Today I will bring Let's interpret the volatile keyword in detail.

    Before that, let's take a look at the current computer hardware memory architecture diagram:

Early computers did not cache this thing. Later, the efficiency of frequently reading data between CPU and memory was too slow. In fact, there is still a faint shadow of the twenty-eight theorem. When the CPU core wants to fetch data, it will hit the cache first, and if not, it will read the cache line from memory. In this system architecture the place where memory interacts with other processors is on the PCI bus. The cache processors are invisible to each other when processing their respective contents, so thread safety problems will arise when reading and storing operations under multi-threaded conditions. At this time, the operating system provides us with two solutions: bus locks And cache locks, bus locks are actually forcibly occupying the entire bus when a thread reads a certain data and not letting other CPUs process it. Obviously, this method is extremely inefficient. The implementation of the cache lock mainly relies on the Lock prefix instruction, which will flush the content of the current cache line back to the main memory, and ensure the consistency of the data through the Cache Coherence Protocol ( MESI ). In Java's memory model, there is a similar model:

However, this local memory (thread private) is an abstract concept and does not really exist. To understand the thread safety issues in java, we must first know that the multi-threaded communication method in java includes shared variables and message passing. The way of sharing variables is transparent to programmers, that is, implicitly passing messages, so once there is a multi-thread safety problem, it is easy for people who do not understand the principle to be overwhelmed. We know that heap memory is shared by all threads, instance objects, static variables and array objects stored in it can be used as our communication medium. After JDK1.5, java uses the JSR-133 memory model, and uses the happens-before principle to describe the memory visibility between operations. Generally speaking, we have more related principles (represented by hb here):

        1. Any operation hb in a single thread is performed in subsequent operations

        2. The operation of unlocking hb and acquiring a lock for a lock

        3. A write operation hb to a volatile variable is a read operation of the volatile variable

        4. If A hb B, B hb C, then A hb C

How to understand hb? As mentioned earlier, this principle is used to describe the memory visibility between operations, which means that in a single thread, the results of previous operations should be visible to subsequent results. In other words, the results of all operations that change the value are visible to reading, which ensures consistency when reading. So how is this principle implemented in JMM? The answer is to prohibit reordering. First, let's understand why there is reordering.

        In pursuit of performance, Java developers have optimized the compiler and processor respectively, which allows the compiler to rearrange the execution order of statements without affecting the specific semantics of the program, such as a=0;b=0;: The exchange of the before and after operations of these two operations does not affect the understanding of the subsequent operations of the second statement. At this time, the assignment of b can be first. The emergence of processor reordering is because modern processors use instruction parallelism to overlap execution of multiple instructions, so the execution order of machine instructions corresponding to statements can be changed without affecting data dependencies.

        As mentioned earlier, volatile variables are accomplished by disabling reordering, and disabling reordering is accomplished by instructions that use memory barriers. JMM divides memory barrier instructions into four categories: LoadLoad, LoadStore, StoreLoad, and StoreStore. From my understanding, store may be regarded as a stored value, and load as a read value will be easier to remember. For the instruction before Load, it is ensured that the previous read instruction will precede the subsequent read/store instruction, while the instruction in front of Store is to ensure that the previous store instruction (flush to memory) will affect the subsequent store and storage instructions. read visible. However, StoreLoad is a special instruction. While the previous conditions are met, it will wait for all instructions before the barrier to complete before starting to execute subsequent instructions.

        JMM provides a conservative memory barrier insertion for volatile variables. It adds StoreStore before each volatile write operation, adds StoreLoad after write, adds LoadLoad before read, and adds LoadStore after read (it's easy to remember). From the instructions it gives, we can know that the write operation before the volatile write operation will be visible to this operation, and the same is true for others.

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=325764249&siteId=291194637