Java JUC concurrent package (2) - understanding JMM from the perspective of JVM

Java memory area

The Java memory area is real, and the JVM divides the automatically managed memory into 5 different areas during the running of the program. The Java memory area division diagram is as follows:

write picture description here

Thread data private area: As threads are created and dies, there is no need to think too much about memory reclamation, and the required memory size is determined at compile time

  • Method area:
    The method area is a data area shared by threads, and is mainly used to store class information that has been loaded by the virtual machine: constants, static variables, code compiled by the real-time compiler, and other data. When the method area cannot meet the memory allocation requirements, an OutOFMemoryError exception will be thrown. It is worth noting that there is an area called the Runtime Constant Pool in the method area, which is mainly used to store various literal values ​​and symbolic references generated by the compiler. These contents are stored in the runtime constant after class loading. in the measuring pool for subsequent use.
    【Notice】

    1. The size of the method area does not have to be fixed or contiguous
    2. The method can be garbage collected. When a class is not used, the JVM will perform garbage collection
    3. The content stored in the method area:
      (1) The full path name of
      the class (2) The full path name of the direct superclass of the class
      (3) The type of the class (whether it is a class or an interface)
      (4) The access modifier of the class: public private, etc.
      (5) Ordered list of the fully qualified names of the direct interface of the class
      (6) Constant pool (field, method information, static variable, type reference class)
      Class variable: It is a static variable, and there is a static area in the method area, which is specially used for to store static variables and static blocks
  • The heap
    is also a memory shared by threads. It is created when the JVM starts. It is the largest piece of memory managed by the Java virtual machine and is mainly used to store object instances . Almost all object instances allocate memory here. The Java heap is the main area for garbage collection management, so many call it the GC heap. If there is no memory in the heap to complete the instance allocation, and the heap can no longer be expanded, it will throw OutOfMemoryError exception

Stack: In the JVM, the stack is used to store some object references, local variables and intermediate data in the calculation process. These variables are also destroyed after the method exits. Its storage is much faster than the heap, only slower than the registers in the CPU, and the default size in the JVM is 1M

  • Program counter:

This area is the only one that does not specify any OutOfMemoryError conditions in the JVM specification.
It belongs to the thread private data area and is a small piece of memory space, which mainly represents the bytecode line number indicator executed by the current thread . When the bytecode interpreter is working, it selects the next bytecode instruction that needs to be executed by changing the value of this counter. Basic functions such as branching, looping, jumping, exception handling, and county restoration all need to be completed by this counter.

  • Virtual machine stack:

A data area that is private to the thread, created at the same time as the thread, the total number is associated with the thread, and represents the memory model of Java method execution. When each method is created, a stack frame is created to store the method's variable table, operand stack, dynamic link method, return value, return address and other information. Each method corresponds to the stacking and popping process of a stack frame in the virtual machine stack from the call to the end.

Memory model JMM (Java Memory Mode)

Introduction

Due to the gap of several orders of magnitude between the storage device of the computer and the computing power of the processor, modern computer systems have to add a layer of cache with a read and write speed as close as possible to the computing speed of the processor as a memory and a cache. Buffering between processors: Copy the data needed for the calculation into the cache, so that the calculation can be performed quickly, and then synchronize back to the memory from the cache after the operation is completed, so that the processor does not need to wait for slow memory read and write. .

The storage interaction based on the tell cache solves the contradiction between the speed of the processor and the memory well, but introduces a new problem: Cache Coherence . In a multiprocessor system, each processor has its own cache, and they share a memory, as shown in the following figure: Multiple processors involve the same piece of storage, and a protocol is needed to ensure data consistency This kind of protocol is also: MESI, MESI, MOSI and Dragon Protocol, etc.

write picture description here

In addition, in order to make full use of the arithmetic unit inside the processor, the processor may perform out-of-order execution (Out of Order Execution) optimization on the input code, and the processor will execute the heap out of order after the calculation. The code reorganizes the results to ensure the accuracy of the results. Similar to the processor's out-of-order execution optimization, the Java virtual machine's just-in-time compiler also has a similar instruction reordering (instruction recorder) optimization.

Java memory model

The memory model can be understood as the abstraction of the process of writing a specific operation and reading and writing access to a specific memory or cache. Physical machines under different architectures have different memory models, and Java virtual machines also have their own memory models, namely Java Memory Model JJM (Java Memory Mode). Direct use of physical hardware and operating system memory model in C/C++ language leads to concurrent access errors under different platforms. The emergence of JMM can shield the memory access differences of various hardware and operating systems, achieve platform consistency, and enable Java programs to "compile once and run everywhere" .

main memory and working memory

The entity of the JVM running program is a thread, and when each thread is created, the JVM creates a working memory (also called stack space) for storing thread-private data, and the Java memory model stipulates that all variables are Stored in the main memory, the main memory is a shared memory area, all threads can access the main memory, the thread wants to operate on the variable (read assignment, etc.) must be performed in the working memory. First, copy the variable from the main memory to its own working memory, then operate the variable in the working memory, and then write the variable back to the main memory after the operation is completed. You cannot directly operate the variable in the main memory. Because the working memory is a private area of ​​each thread, different threads cannot access the working memory of each other's threads, and the communication between threads needs to go through the main memory.

write picture description here

In some places, the main memory is described as heap memory, and the working memory is called the thread stack

  • Main memory
    mainly stores Java instance objects, and instance objects created by all threads are stored in the main memory, regardless of whether the instance object is a member variable or a local variable in a method, of course, it also includes shared class information, constants, and static variables. .

  • The working memory
    mainly stores all the local variable information of the current method (the working memory stores a copy of the variable copy in the main memory), each thread can only access its own working memory, that is, the local variables in the thread are invisible to other threads , even if the two threads execute the same piece of code, they will each create local variables belonging to the current thread in their own working memory, of course, including the bytecode line number control period and related native method information. Note that since the working memory is the private data of each thread, the threads cannot access the working memory with each other, and there is no security problem with the data stored in the working memory.

memory interaction

The JMM model defines eight operations to complete the transfer of data from main memory to working memory

  • (1) lock (lock): a variable acting on the main memory, placing a variable flag in the exclusive state of a thread
  • (2) unlock (unlock): a variable that acts on the main memory, releases a variable in a locked state, and the released variable can be locked by other threads
  • (3) read (read): acts on the main memory variable, and transfers a variable value from the main memory to the working memory of the thread for use by the subsequent load action
  • (4) load (load): acting on the working memory variable, it puts the variable value obtained by the read operation from the main memory into the variable copy of the working memory
  • (5) use (use): acting on working memory variables, passing a variable value in working memory to the execution engine, this operation will be executed whenever the virtual machine encounters a bytecode instruction that needs to use the value of the variable
  • (6) assign (assignment): a variable acting on working memory, which assigns a value received from the execution engine to a variable in working memory, and executes this operation whenever the virtual machine encounters a bytecode instruction that assigns a variable to a variable
  • (7) store (storage): a variable that acts on the working memory, and transfers the value of a variable in the working memory to the main memory for subsequent write operations.
  • (8) write (write): used for variables in main memory, it transfers the value of a variable in working memory to a variable in main memory by store operation.

The JVM only requires copying from the main memory to the working memory, which needs to be executed in order: (read and store); if it is synchronized from the working memory back to the main memory, it must be executed in order: (store and write). JMM only requires that the above two types of operations must be performed in order, and there is no guarantee that they must be performed consecutively. That is, between read and store, other instructions can be inserted. JMM also stipulates that when performing the above eight basic operations, the following rules must be met:

  • (1) One of read and load, store and write operations is not allowed to appear alone
  • (2) A thread is not allowed to discard its most recent assign operation, that is, variables must be synchronized to main memory after being changed in working memory
  • (3) A thread is not allowed to synchronize data from working memory to main memory for no reason (no assign operation has occurred)
  • (4) A new variable can only be born in the main memory, and it is not allowed to use an uninitialized (load or assign) variable directly in the working memory. That is, before implementing use and store on a variable, restrictive assign and load operations must be performed
  • (5) A variable only allows one thread to lock it at the same time, and lock and unlock must appear in pairs
  • (6) If the lock operation is performed on a variable in the main memory, the value of the variable in the working memory will be cleared. Before the execution engine uses the variable, the load or assign operation needs to be re-executed to initialize the value of the variable.
  • (7) If a variable is not locked by the lock operation in advance, it is not allowed to be unlocked, and it is not allowed to unlock a variable that is locked by other
  • (8) Before performing the unlock operation on a variable, the variable must be synchronized to the main memory (execute store and write operations)

Java uses a happens before principle to determine whether a memory access is safe in a concurrent environment

The meaning of JMM's existence

A fundamental problem with multithreading is the sharing of variables between threads

Variables in Java are divided into three categories:
(1) class variables
(2) instance variables
(3) local variables (variables declared in methods)

Suppose: there is a shared variable x=1 in the main memory variable, and now there are two threads A and B that add 1 and read the variable x respectively, and there are shared variables in the working memory of threads A and B. copy x. Suppose now that thread A wants to perform a +1 operation, and thread B wants to perform a read X operation, then the value read by thread B is the value of x after thread A's update or the value of x before the update? The answer is: not sure; B may read the value of x as 1 before A's update, or 2 after update. This may cause data inconsistency between the main memory and the working memory, which is the so-called thread safety problem.

In order to solve the problem of thread safety, the JVM implements a set of rules that determine when a thread's writing to a shared variable is visible to another thread. This set of rules also becomes the Java Memory Model JMM. JMM is built around the program. The atomicity, orderliness, and visibility of execution are unfolded.

Atomicity, Order, and Visibility

atomicity

Atomicity means that an operation is uninterruptible, even in a multithreaded environment, once an operation starts, it will not be affected by other threads. For example, for a static variable int x, two threads assign values ​​to it at the same time, thread A is assigned a value of 1, and thread B is assigned a value of 2, no matter how the thread runs, the final value of x is either 1 or 2, thread A and thread There is no interference between operations between B, which is the characteristic of atomic operations that cannot be interrupted. It should be noted that for 32-bit systems, long type data and double type data (for basic data types, byte, short, int, float, boolean, char read and write are atomic operations), their read and write is not Atomic, that is to say, if there are two threads reading and writing data of long type or double type at the same time, there is mutual interference, because for a 32-bit virtual machine, each atomic read and write is 32-bit, and long and double are 64-bit storage units, which will cause a thread to read only the last 32-bit data when it is the turn of thread B to read after the atomic operation of the first 32-bit is performed. A variable that is neither the original value nor the thread-modified value may be read. It may be the value of "half a variable", that is, the 64-bit data is divided into two reads by two threads. But don't worry too much, because it is rare to read "half a variable", at least in current commercial virtual machines, almost all read and write operations of 64-bit data are performed as atomic operations, so for this Don't worry too much about the problem, just know what's going on.

Classification of instruction rearrangements

When the computer executes the program, in order to improve the performance, the compiler and the processor often rearrange the instructions, which are generally divided into the following three types:

  • Compiler optimization rearrangement (belonging to compiler rearrangement)
    The compiler can rearrange the execution order of statements without changing the semantics of a single-threaded program
  • Instruction-parallel reordering (which is part of processor reordering)
    Modern processors employ instruction-level parallelism to overlap the execution of multiple instructions. If there is no data dependency (that is, the later executed statement does not need to depend on the result of the earlier executed statement), the processor can change the execution order of the machine instructions corresponding to the statement
  • The rearrangement of the memory system (belonging to the processor rearrangement)
    Because the processor uses the cache, this makes the load and store operations appear to be executed out of order. Because of the existence of the L3 cache, there is a time difference between the data synchronization between the memory and the cache.

Compiler rearrangement

example:
write picture description here

The execution result of the program may appear as x1 = 1, x2 = 2. If the compiler rearranges the program code, the following situations may occur

write picture description here
After rearrangement, x1 = 1, x2 = 2 may appear, which means that in a multi-threaded environment, due to the existence of compiler optimization and rearrangement, it is uncertain whether the variables used in the two threads can guarantee consistency.

visibility

Visibility refers to whether when a thread modifies the value of a shared variable, other threads can immediately know the modified value. For serial programs, visibility does not exist, because we modify the value of a variable in any operation, and subsequent operations can read the variable value, and it is the modified new value. But in a multi-threaded environment, this is not necessarily the case. Since the operation of shared variables by threads is that threads copy data from the shared area to their respective working memory, and then operate the data and then write it back to the main memory, there may be a thread A. When the value of the shared variable x is modified and has not been written back to the main memory, another thread B operates on the same shared variable x in the main memory, but at this time the shared variable x in the working memory of thread A is not available to thread B. It can be seen that this phenomenon of synchronization delay between working memory and main memory causes visibility problems. In addition, instruction rearrangement and compiler optimization can also cause visibility problems.

orderliness

Ordered means that for single-threaded execution code, we always think that the execution of the code is executed in order. However, for multi-threading, after the program is compiled into machine code instructions, instruction rearrangement may occur, and the rearranged instructions may not be in the same order as the original instructions.

JVM solves thread safety problem

  • Solution to the atomicity problem:

    • (1) JVM itself provides atomicity of read and write operations on basic data types
    • (2) For atomic operations at the method level or code block level: there is the synchronized keyword or the reentrant lock (ReentrentLock) to ensure the atomicity of the program
  • Solution to the problem of synchronization delay between working memory and main memory

    • It can be solved by using the volatile keyword
  • In addition to the above solutions, JMM also defines a set of hapens-before principles to ensure atomicity, visibility and ordering between two operations in multiple environments.

happens-before principle

This principle is used to judge whether there is competition in the data and whether the thread is safe. The principle is as follows: The content in [] is "In-depth Understanding of Java Virtual Machine Chapter 12"

  • (1) The principle of program order: within a thread, according to the code order, the operations written in the front occur first before the operations written in the back; [The result of a piece of code executed in a single thread is ordered. Note that it is the execution result, because the virtual machine and the processor will reorder the instructions (reordering will be described in detail later). Although it is reordered, it does not affect the execution result of the program, so the final execution result of the program is consistent with the sequential execution result. Therefore, this rule is only valid for a single thread, and the correctness cannot be guaranteed in a multi-threaded environment.
  • (2) Locking rules: an unlock operation occurs first before the lock operation of the same lock later [whether in a single-threaded environment or a multi-threaded environment, a lock is locked, then the unlock operation must be performed first before the lock can be performed. operate.
  • (3) volatile variable rule: the write operation to a variable occurs first before the read operation of the variable [if a thread writes a volatile variable first, and then a thread reads the variable, then the write operation must happen -before read operation]
  • (4) Delivery rule: If operation A occurs first in operation B, and B occurs first in C, then it can be concluded that A occurs first in C
  • (5) **Thread startup rules: **The start() method of the Thread object occurs first for every action of this thread
  • (6) Thread interruption rules: The call to the thread interrupt() method occurs first in the code of the interrupted thread and detects the occurrence of the interrupt event. You can use the Thread.interrupted() method to detect whether the thread is interrupted.
  • (7) Thread termination rules: All operations in the thread occur first in the thread termination detection. We can detect that the thread has terminated execution through the end of the Thread.join() method and the return value of Thread.isAlive().
  • (8) Object finalization rules: the initialization of an object occurs first at the beginning of its finalize() method

Reference blog post

https://blog.csdn.net/u011080472/article/details/51337422
https://blog.csdn.net/javazejian/article/details/72772461
https://www.cnblogs.com/chenssy/p/6393321.html

Guess you like

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