- An overview of the
Multitasking and high concurrency is one of the important indicators to measure the capability of a computer processor. Transactions Per Second (TPS) is a better indicator to measure the performance of a server. It represents the number of requests that the server can respond to on average in one Second, and TPS value has a very close relationship with the concurrency of the program. Before discussing the Java memory model and threads, let’s take a quick look at hardware efficiency and consistency.
Because of the difference of several orders of magnitude between the computer’s memory and the processor’s computing power, modern computer systems have to buffer between memory and the processor by adding a layer of caches, which can read and write data as fast as possible to the processor’s computing speed: Copying the data needed for an operation to the cache allows the operation to proceed quickly and then synchronizing it back from the cache when the operation is done so that the processor doesn’t have to wait for slow memory reads and writes. Cache-based storage interaction resolves the processor-memory speed conflict nicely, but introduces a new problem: Cache Coherence. In a multi-processor system, each processor has its own cache and they share the same main memory, as shown in the figure below: Multiple processor computing tasks all involve the same main memory, so a Protocol is needed to ensure data consistency, such as MSI, MESI, MOSI and Dragon Protocol. The memory access operations defined in the Java VIRTUAL machine memory model are comparable to hardware cache access operations, and the Java memory model is described later.
In addition, the processor may optimize the input code for out-of-order Execution in Order to maximize the number Of units within the processor. The processor reorganizes the out-of-order code after the computation to ensure the accuracy Of the results. Similar to out-of-order execution optimization for processors, there are similar Instruction Recorder optimizations in the Just-in-time compiler of the Java virtual machine.
Defining a Java memory model is not an easy task, and the model must be defined rigorously enough so that concurrent Operations in Java are not ambiguous. However, it must be loose enough that the implementation of the virtual machine has the freedom to take advantage of various hardware features (registers, caches, etc.) for better execution speed. After a long period of validation and tinkering, the Java memory model has matured and refined since the JDK1.5 release.
3.1 Main and Working Memory The main goal of the Java memory model is to define the rules for accessing variables in a program, the low-level details of storing variables into and out of memory in the virtual machine. Variables are not the same as variables in Java programming. They include instance fields, static fields, and elements that make up array objects, but not local variables and method parameters, which are thread private and not shared.
Java memory model specified in all the variables are stored in main memory, each thread has its own working memory (can and will be in front of the processor cache analogy), thread of working memory to save the thread to use the variable to the main memory copy of a copy of the thread of variables all the operations (read, assignment) must be done in working memory, You cannot read or write variables directly from main memory. Different threads cannot directly access variables in each other’s working memory, and the transfer of variable values between threads needs to be completed in the main memory. The interaction between threads, main memory and working memory is shown in the figure below, which is similar to the figure above.
The main memory and working memory are not in the same hierarchy as the Java heap, stack, and method areas of the Java memory area.
3.2 Inter-Memory Interaction Regarding the specific interaction protocol between main memory and working memory, that is, how a variable is copied from main memory to working memory, and how it is synchronized from working memory to main memory, the Java memory model defines the following eight operations to accomplish:
Lock: A variable that acts on main memory and identifies a variable as a thread-exclusive state. Unlock: Applies to a main memory variable. It releases a locked variable before it can be locked by another thread. Load: A working memory variable that places the value of the read operation from main memory into a copy of the working memory variable. Use: variable applied to working memory, passing the value of a variable in working memory to the execution engine. This operation is performed whenever the virtual machine reaches a bytecode instruction that requires the value of the variable to be used. Assign: a working memory variable that assigns a value received from the execution engine to the working memory variable. This operation is performed whenever the virtual machine accesses a bytecode instruction that assigns a value to the variable. Store: Variable applied to working memory that transfers the value of a variable in working memory to main memory for subsequent write operations. Write: a variable operating on main memory that transfers store operations from the value of a variable in working memory to a variable in main memory. To copy a variable from main memory to working memory, read and load operations are performed sequentially, while to synchronize variables from working memory back to main memory, store and write operations are performed sequentially. The Java memory model only requires that the above operations be performed sequentially, but does not guarantee that they must be performed consecutively. Other instructions can be inserted between read and load, store and write. For example, when accessing variables A and B in main memory, the possible order is read A, read B, load B, load A. The Java memory model also specifies that the following rules must be met when performing the eight basic operations described above:
Do not allow one of the read and load, store and write operations to occur separately. Do not allow a thread to discard its most recent assign operation, i.e. variables that have changed in working memory must be synchronized to main memory. A thread is not allowed to synchronize data from working memory back to main memory without a cause (without any assign operation). A new variable can only be created in main memory. It is not allowed to directly use an uninitialized (load or assign) variable in working memory. Assign and load operations must be performed before use and store operations can be performed on a variable. A variable can only be locked by one thread at a time. Lock and unlock must occur in pairs. If you lock a variable, it will clear the working memory of that variable. Before the execution engine can use this variable, load or assign operations must be performed to initialize the value of the variable. If a variable has not been locked by a LOCK operation, unlock cannot be performed on it. It is also not allowed to unlock a variable that has been locked by another thread. Before an unlock operation can be performed on a variable, the variable must be synchronized to main memory (store and write operations). 3.3 Reordering Instructions are often reordered by compilers and processors to improve performance when executing a program. There are three types of reordering:
Compiler optimized reordering. The compiler can rearrange the execution order of statements without changing the semantics of a single-threaded program. Instruction – level parallel reordering. Modern processors use instruction-level parallelism to superimpose multiple instructions. If there is no data dependency, the processor can change the execution order of the machine instructions corresponding to the statement. Memory system reordering. Because the processor uses caching and read/write buffers, this makes it appear that load and store operations may be performed out of order. The sequence of instructions from the Java source code to the actual execution is reordered in one of three ways:
To ensure memory visibility, the Java compiler inserts memory barrier instructions at the appropriate places to generate instruction sequences to prohibit reordering of a particular type of handler. The Java memory model divides memory barriers into LoadLoad, LoadStore, StoreLoad, and StoreStore:
3.4 Synchronization This topic describes volatile, synchronized, and final synchronization
3.5 Atomicity, Visibility, and Orderliness This section describes the three properties