Network Security Internet Technology Development Database Servers Mobile Phone Android Software Apple Software Computer Software News IT Information

In addition to Weibo, there is also WeChat

Please pay attention

WeChat public account

Shulou

Detailed explanation of Java memory area and memory Model

2025-01-31 Update From: SLTechnology News&Howtos shulou NAV: SLTechnology News&Howtos > Development >

Share

Shulou(Shulou.com)06/03 Report--

This article mainly introduces "detailed explanation of Java memory region and memory model". In daily operation, I believe many people have doubts about the detailed interpretation of Java memory region and memory model. Xiaobian consulted all kinds of materials and sorted out simple and easy-to-use operation methods. I hope it will be helpful for you to answer the doubts of "Java memory region and memory model detailed explanation". Next, please follow the editor to study!

Brief introduction

First, two nouns are introduced: 1) visibility: a thread's modification of the value of a shared variable can be seen by other threads in a timely manner. 2) shared variable: if a variable has a copy in the working memory of multiple threads, then this variable is the shared variable of these threads.

The communication between Java threads is completely transparent to programmers. In concurrent programming, two key issues need to be dealt with: how to communicate between threads and how to synchronize between threads.

Communication: communication refers to the mechanism by which threads exchange information. In imperative programming, there are two communication mechanisms between threads: shared memory and message passing. In the concurrency model of shared memory, threads share the common state of programs and communicate implicitly by writing-reading the common state in memory. In the concurrency model of message passing, there is no common state between threads, and threads must communicate with each other by sending messages.

Synchronization: synchronization is a mechanism used in a program to control the relative order of operations between different threads. In the shared memory concurrency model, synchronization is displayed, and the programmer must show that a specified method or piece of code needs to be mutually exclusive between threads. In the concurrency model of message delivery, synchronization is implicit because the message must be sent before it is received.

Java concurrency uses a shared memory model.

1. Java memory area (JVM memory area)

When the Java virtual machine runs the program, it divides its automatically managed memory into the above areas, each of which has its own purpose and the timing of creation and destruction, in which the blue part represents the data area shared by all threads, while the green part represents the private data area of each thread.

Method area (Method Area):

The method area belongs to the memory area shared by threads, also known as Non-Heap (non-heap). It is mainly used to store class information, constants, static variables, code compiled by instant compiler and other data that have been loaded by the virtual machine. According to the Java virtual machine specification, when the method area can not meet the memory allocation requirements, an OutOfMemoryError exception will be thrown. It is worth noting that there is an area in the method area called the runtime constant pool (Runtime Constant Pool), which is mainly used to hold various literals and symbolic references generated by the compiler, which will be stored in the runtime pool after the class is loaded for later use.

JVM heap (Java Heap):

Java heap is also a memory area shared by threads. It is created when the virtual machine starts, and is the largest piece of memory managed by the Java virtual machine. It is mainly used to store object instances. Almost all object instances are allocated memory here. Note that Java heap is the main area managed by garbage collector, so it is often called GC heap. If there is no memory in the heap to complete instance allocation, and the heap can no longer be expanded. An OutOfMemoryError exception will be thrown.

Program counter (Program Counter Register):

A data area that is private to a thread and is a small piece of memory space that mainly represents the bytecode line number indicator executed by the current thread. When the bytecode interpreter works, by changing the value of this counter to select the next bytecode instruction to be executed, branch, loop, jump, exception handling, thread recovery and other basic functions need to rely on this counter to complete.

Virtual Machine Stack (Java Virtual Machine Stacks):

A data region that is private to a thread, created at the same time as the thread, and the total is associated with the thread, representing the memory model of the Java method execution. Only references to the underlying data type and custom objects (not objects) are saved in the stack, and the objects are stored in the heap area. When each method is executed, a stack frame is created to store the variable table, Operand stack, dynamic link method, return value, return address and other information of the method. Each method ends directly from the call for the process of entering and exiting a stack frame in the virtual machine stack, as follows (figure error, should be stack frame):

Local method stack (Native Method Stacks):

The local method stack belongs to the private data area of the thread, which is mainly related to the Native method used by the virtual machine. In general, we do not need to care about this area.

2. Java memory model

The Java memory model (Java Memory Model, referred to as JMM) itself is an abstract concept and does not really exist. Communication between Java threads is controlled by JMM, and JMM determines when writes by one thread to shared variables are visible to another thread. From an abstract point of view, JMM defines the abstract relationship between threads and main memory.

Because the entity of the JVM running program is a thread, and when each thread is created, JVM will create a working memory (in some places called stack space) for it to store the private data of the thread, while the Java memory model stipulates that all variables are stored in the main memory, which is a shared memory area and can be accessed by all threads, but the operation of the thread on the variables (reading assignment, etc.) must be carried out in the working memory.

First of all, copy the variable from the main memory to its own working memory space, and then operate on the variable, and then write the variable back to the main memory after the operation is completed. You cannot directly manipulate the variable in the main memory. The copy of the variable in the main memory is stored in the working memory. As mentioned earlier, the working memory is the private data area of each thread, so different threads cannot access each other's working memory. The communication between threads (passing values) must be done through the main memory, and the brief access process is as follows

Figure 3

It should be noted that the division of memory areas between JMM and Java is a different conceptual level. More appropriately, JMM describes a set of rules that control how variables in the program are accessed in shared and private data areas. JMM revolves around atomicity, order, and visibility (which will be analyzed later).

The only similarity between JMM and Java memory area is that there are shared data area and private data area. In JMM, the main memory belongs to shared data area, which to some extent should include heap and method area, while the private data area of working memory data thread should include program counter, virtual machine stack and local method stack to some extent. Perhaps in some places, we may see that main memory is described as heap memory, and working memory is called thread stack, but in fact they all mean the same thing. The main memory and working memory in JMM are described as follows

Main memory

Java instance objects and shared variables between threads are mainly stored. Instance objects created by all threads are stored in main memory, regardless of whether the instance object is a member variable or a local variable in the method (also known as local variable). Of course, it also includes shared class information, constants and static variables. Because it is a shared data region, multiple threads accessing the same variable may find thread safety problems.

Some books, also known as local memory, mainly store all the local variable information of the current method (a copy of the variable in the main memory is stored in the working memory). Each thread can only access its own working memory, that is, the local variables in the thread are not visible to other threads, even if the two threads are executing the same piece of code. They also create local variables that belong to the current thread in their own working memory, including bytecode line number indicators and related Native methods.

Note that because working memory is private to each thread, threads cannot access each other's working memory, so there is no thread safety problem with the data stored in working memory. Note that working memory is an abstract concept of JMM and is not real.

After figuring out the main memory and working memory, then understand the data storage type and operation mode of the main memory and working memory. According to the virtual machine specification, for a member method in an instance object, if the local variable in the method is the basic data type (boolean,byte,short,char,int,long,float,double), it will be stored directly in the frame stack structure of the working memory, but if the local variable is a reference type. Then the reference to the variable is stored in the frame stack of functional memory, while the object instance is stored in main memory (shared data area, heap).

However, member variables of instance objects, whether they are basic data types or wrapper types (Integer, Double, etc.) or reference types, are stored in the heap area. As for the static variable and the information about the class itself, it will be stored in main memory. It should be noted that the instance object in the main memory can be shared by multiple threads. If two threads call the same method of the same object at the same time, the two threads will copy the data to be operated into their working memory and refresh it to the main memory after the operation is completed. The simple diagram is as follows:

Figure 4

From figure 3, if you want to communicate between thread An and thread B, you must go through the following two steps:

1) Thread A flushes the updated shared variables in local memory A to main memory

2) Thread B goes to main memory to read the shared variables that have been updated before Thread A

From the above two steps, the shared memory model completes the process of "implicit communication".

JMM also provides memory visibility for Java programmers by controlling the interaction between the main memory and the working memory of each thread.

III. As-if-serial semantics, happens-before principles 3.1 as-if-serial semantics

Reordering is a means for compilers and processors to reorder instruction sequences in order to optimize program performance. As-if-serial semantics means that no matter how much you reorder (compilers and processors to improve parallelism), the execution results of (single-threaded) programs cannot be changed. Compilers, runtime, and processors must all follow as-if-serial semantics. In order to comply with as-if-serial semantics, compilers and processors do not reorder operations that have data dependencies because such reordering changes the execution result.

However, if there are no data dependencies between operations, they may be reordered by the compiler and processor.

3.2 happens-before principles

Happens-before is the core concept of JMM. For Java programs, understanding happens-before is the key to understanding JMM.

There are two key factors to consider when designing a JMM:

Programmers' use of the memory model. Programmers want the memory model to be easy to understand and program. Programmers want to write code based on a strong memory model.

The implementation of memory model by compiler and processor. Compilers and processors want the memory model to bind them as little as possible so that they can do as much optimization as possible to improve performance. Compilers and processors want to implement a weak memory model.

But the above two points contradict each other, so the core membrane table of the JSR-133 expert group when designing JMM is to find a good balance: on the one hand, to improve enough memory visibility for programmers; on the other hand, the restrictions on compilers and processors are relaxed as much as possible.

Another particularly interesting thing is about reordering. To put it more simply, reordering can be divided into two categories: 1) it will change the reordering of program execution results. 2) the reordering of program execution results will not be changed.

JMM adopts different strategies for the reordering of these two different properties, as follows:

For reordering that changes the result of program execution, JMM requires that compilers and processors must prohibit such reordering.

JMM does not require compilers and processors for reordering that does not change the result of program execution (JMM allows such reordering)

The design of JMM is as follows:

JMM design schematic diagram can be seen from the diagram:

The happens-before rules provided by JMM to programmers can meet the needs of programmers. JMM's happens-before rules are not only easy to understand, but also provide programmers with sufficient memory visibility guarantees (some memory visibility guarantees are not necessarily real, such as A happens-before B above).

JMM has as few constraints on compilers and processors as possible. As can be seen from the above analysis, JMM is actually following a basic principle: as long as the execution result of the program is not changed (refers to single-threaded programs and correctly synchronized multithreaded programs), the compiler and processor can be optimized. For example, if the compiler, after careful analysis, determines that a lock will only be accessed by a single thread, the lock can be eliminated. For example, if the compiler, after careful analysis, determines that a volatile variable will only be accessed by a single thread, then the compiler can treat the volatile variable as a normal variable. These optimizations will not change the execution result of the program, but also improve the execution efficiency of the program.

3.3 happens-before definition

The concept of happens-before was first proposed by Leslie Lamport in a far-reaching paper ("Time,Clocks and the Ordering of Events in a Distributed System"). JSR-133 uses the concept of happens-before to specify the order of execution between two operations. Because these two operations can be within one thread or between different threads. Therefore, JMM can provide programmers with cross-thread memory visibility through happens-before relationships (if there is an happens-before relationship between thread A's write operation an and thread B's read operation b, although an operation and b operation are performed in different threads, JMM assures the programmer that an operation will be visible to b operation). It is specifically defined as:

1) if one operation happens-before another operation, the execution result of the first operation will be visible to the second operation, and the execution order of the first operation comes before the second operation.

2) the existence of a happens-before relationship between the two operations does not mean that the specific implementation of the Java platform must be executed in the order specified by the happens-before relationship. If the execution result after the reorder is consistent with the result performed according to the happens-before relationship, then this reorder is not illegal (that is, JMM allows such a reorder).

1) above is JMM's commitment to programmers. From the programmer's point of view, the happens-before relationship can be understood as follows: if A happens-before B, then the Java memory model will assure the programmer that the results of An operations will be visible to B and that A will be executed before B. Note that this is just a guarantee made by the Java memory model to programmers!

2) above is the constraint principle of JMM for compiler and processor reordering. As mentioned earlier, JMM is actually following a basic principle: as long as the execution result of the program is not changed (that is, single-threaded programs and correctly synchronized multithreaded programs), the compiler and processor can optimize whatever they want. The reason JMM does this is that programmers don't care whether these two operations are really reordered, but that the semantics of program execution cannot be changed (that is, the execution results cannot be changed). Therefore, the happens-before relationship is essentially the same thing as as-if-serial semantics.

3.3 happens-before vs. as-if-serial

As-if-serial semantics ensures that the execution results of programs in a single thread will not be changed, and happens-before relations ensure that the execution results of correctly synchronized multithreaded programs will not be changed.

As-if-serial semantics creates an illusion for programmers who write single-threaded programs: single-threaded programs are executed in the order of the program. The happens-before relationship creates an illusion for programmers who write correctly synchronized multithreaded programs: correctly synchronized multithreaded programs are executed in the order specified by happens-before.

The purpose of as-if-serial semantics and happens-before is to improve the parallelism of program execution as much as possible without changing the result of program execution.

3.4 happens-before specific rules

Program order rules: each operation in a thread, happens-before any subsequent operations in that thread.

Monitor lock rule: unlock a lock, which is subsequently locked by happens-before.

Volatile variable rule: write to a volatile domain, happens-before any subsequent reads to that volatile domain.

Transitivity: if A happens-before B and B happens-before C, then A happens-before C.

Start () rule: if thread An executes the operation ThreadB.start () (starts thread B), then thread A's ThreadB.start () operation happens-before any operation in thread B.

Join () rule: if thread An executes the operation ThreadB.join () and returns successfully, then any operation happens-before in thread B returns successfully from the ThreadB.join () operation on thread A.

3.5 the relationship between happens-before and JMM

An happens-before rule corresponds to one or more compiler and processor reordering rules. For Java programmers, happens-before rules are easy to understand and prevent Java programmers from learning complex reordering rules and how to implement them in order to understand the memory visibility guarantees provided by JMM.

Memory semantics of volatile and locks 4.1 memory semantics of volatile

When a shared variable is declared to be volatile, the read / write to this variable is very special. A single read / write operation of a volatile variable is synchronized with the read / write operation of a normal variable using the same lock, and the execution effect between them is the same.

The lock's happens-before rule guarantees memory visibility between the thread that releases the lock and the thread that acquires the lock, which means that reading a volatile variable will always see (any thread) the last write to the volatile variable.

The semantics of the lock determines that the execution of the code in the critical area is atomic. This means that even 64-bit long and long variables are atomically read / written to as long as they are volatile variables. In the case of multiple volatile operations or composite operations such as volatile++, these operations are generally not atomic.

In short, once a shared variable (member variable of a class, static member variable of a class) is modified by volatile, there are two layers of semantics:

1) ensures the visibility of different threads when operating on this variable, that is, one thread modifies the value of a variable, and the new value is immediately visible to other threads.

2) instruction reordering is prohibited.

Visibility. When reading a volatiole variable, you can always see (any thread) the last write to the volatile variable.

Order. The volatile keyword prevents instruction reordering, so volatile can guarantee ordering to a certain extent.

The volatile keyword forbids instruction reordering has two meanings:

1) when the program performs the read or write operation of the volatile variable, all the changes in the previous operation must have been made, and the result is already visible to the later operation; the subsequent operation must not have been carried out.

2) when optimizing instructions, statements accessed to volatile variables cannot be executed behind them, nor statements that follow volatile variables can be executed before them.

Maybe the above is a little round. let me give you a simple example:

Because the flag variable is a volatile variable, statement 3 will not be placed before statement 1 and statement 2, and statement 3 will not be placed after statement 4 and statement 5 during instruction reordering. However, it is important to note that the order of statements 1 and 2, and the order of statements 4 and 5 are not guaranteed.

Atomicity. The reading and writing of any single volatile variable is atomic, but the compound operation like volatile++ is not atomic.

Memory semantics written by volatile: when writing a volatile variable, JMM flushes the value of the shared variable in the local memory corresponding to the thread to the main memory.

Memory semantics for volatile reads: when reading a volatile variable, JMM invalidates the local memory setting for that thread. The thread then reads the shared variable from the main memory. Force the shared variables to be read from the main memory, making the values of the shared variables of the local memory consistent with those of the main memory.

Summary of memory semantics for volatile write and read:

Thread A writes a volatile variable, which essentially sends a message to a thread that is about to read the volatile variable.

Thread B reads a volatile variable, which essentially means that thread B receives a message from a previous thread.

Thread A writes a volatile variable, and then thread B reads the volatile variable, which is essentially thread A sending a message to thread B through main memory. (implicit communication)

4.2 implementation of volatile memory semantics

Compiler reordering and processor reordering were mentioned earlier. In order to implement volatile memory semantics, JMM restricts these two types of reordering respectively.

When the second operation is volatile write, no matter what the first operation is, it cannot be reordered. This rule ensures that operations before volatile writing are not reordered by the compiler after volatile writing.

When the first operation is a volatile read, no matter what the second operation is, it cannot be reordered. This rule ensures that operations after volatile read are not reordered by the compiler before volatile read.

When the first operation is volatile write and the second operation is volatile read, it cannot be reordered.

In order to implement the memory semantics of volatile, when generating bytecode, the compiler inserts a memory barrier in the instruction sequence to prevent certain types of processors from reordering. It is almost impossible for the compiler to find an optimal arrangement to minimize the total number of insertion barriers. To this end, JMM adopts a conservative strategy. The following is the JMM memory barrier insertion strategy based on the conservative policy:

Insert a StoreStore barrier in front of each volatile write operation.

Insert a StoreLoad barrier after each volatile write operation.

Insert a LoadLoad barrier after each volatile read operation.

Insert a LoadStore barrier after each volatile read operation.

The above memory barrier insertion strategy is very conservative, but it can ensure that the correct volatile memory semantics can be obtained in any processor platform and any program.

The following is a schematic diagram of the instruction sequence generated after volatile writes are inserted into the memory barrier under a conservative strategy:

The StoreStore barrier in the image above ensures that all normal writes preceding it are visible to any processor before volatile is written. This is because the StoreStore barrier ensures that all normal writes above are flushed to main memory before volatile writes.

What's interesting here is the StoreLoad barrier behind volatile writes. The purpose of this barrier is to prevent volatile writes from being reordered with possible volatile read / write operations. Because the compiler is often unable to accurately determine whether a StoreLoad barrier needs to be inserted after a volatile write (for example, a method return immediately after a volatile write). To ensure that the memory semantics of volatile are correctly implemented, JMM takes a conservative strategy here: insert an StoreLoad barrier after each volatile write or before each volatile read. In terms of overall execution efficiency, JMM chose to insert an StoreLoad barrier after each volatile write. Because the common usage pattern of volatile write-read memory semantics is that one writer thread writes volatile variables, and multiple reading threads read the same volatile variable. When the number of reading threads greatly exceeds the number of writing threads, choosing to insert the StoreLoad barrier after volatile writing will result in a considerable improvement in execution efficiency. From this we can see a feature of JMM in implementation: first to ensure correctness, and then to pursue the efficiency of execution. The following is a schematic diagram of the instruction sequence generated after volatile reads are inserted into the memory barrier under a conservative strategy:

The LoadLoad barrier in the image above is used to prevent the processor from reordering the upper volatile reads with the normal reads below. The LoadStore barrier is used to prevent the processor from reordering the upper volatile read with the lower normal write.

The above memory barrier insertion strategies for volatile writes and volatile reads are very conservative. In actual execution, the compiler can omit unnecessary barriers according to specific circumstances, as long as the write-read memory semantics of volatile is not changed. Let's illustrate it with a specific example code:

Class VolatileBarrierExample {int a; volatile int v1 = 1; volatile int v2 = 2; void readAndWrite () {int I = v1; / / first volatile read int j = v2; / / second volatile read a = I + j; / / ordinary write v1 = I + 1; / / first volatile write v2 = j * 2; / / second volatile write}... / / other methods}

For the readAndWrite () method, the compiler can do the following optimizations when generating bytecode:

Note that the final StoreLoad barrier cannot be omitted. Because the method return immediately after the second volatile is written. At this point, the compiler may not be able to accurately determine whether there will be volatile reads or writes later, and for security reasons, the compiler will often insert a StoreLoad barrier here.

Take the x86 processor as an example, all barriers except the last StoreLoad barrier in the image above will be omitted.

In order to provide a mechanism for communication between threads that is more lightweight than locks, the JSR-133 expert group decided to enhance the memory semantics of volatile: strictly restrict the reordering of volatile variables and ordinary variables by compilers and processors, and ensure that volatile write-read and lock release-acquisition have the same memory semantics.

Because volatile only guarantees atomicity in reading / writing to a single volatile variable, the mutex execution of locks ensures atomicity in the execution of the entire critical section code. Locks are more powerful than volatile in terms of functionality; volatile has advantages in terms of scalability and execution performance.

When a variable is defined as volatile, the visibility of the variable to all threads is guaranteed, that is, when one thread modifies the value of the variable, the new value of the variable is immediately known to other threads. It can be understood that all writes to the volatile variable can be immediately known by other threads. However, this does not mean that operations based on volatile variables are safe under concurrency, because volatile can only guarantee memory visibility, but not atomicity of variable operations. For example, the following code:

/ * * 20 threads are initiated, and each thread performs 10000 self-incrementing operations on the race variable. If the code can be concurrently correctly, * the final race result should be 200000, but the actual running result is less than 200000. * * @ author Colin Wang * / public class Test {public static volatile int race = 0; public static void increase () {race++;} private static final int THREADS_COUNT = 20; public static void main (String [] args) {Thread [] threads = new Thread [threads _ COUNT]; for (int I = 0; I

< THREADS_COUNT; i++) { threads[i] = new Thread(new Runnable() { @Override public void run() { for (int i = 0; i < 10000; i++) { increase(); } } }); threads[i].start(); } while (Thread.activeCount() >

1) Thread.yield (); System.out.println (race);}}

The logical result is 10000, but it is likely to be a value less than 10000 at run time. Some people may say that volatile does not guarantee visibility. If one thread modifies race, another thread should see it immediately. But the operation race++ here is a compound operation, which includes reading the value of race, incrementing it, and then writing it back to main memory.

Suppose thread A, which reads race with a value of 10, is blocked because the volatile rule cannot be triggered because the variable has not been modified.

Thread B also reads the value of race at this time, and the value of race in main memory is still 10, which increases itself, and then is immediately written back to main memory, which is 11.

At this time, it is thread A's turn to execute, because what is saved in the working memory is 10, so continue to do self-increment, write back to main memory, 11 is written again. So although the two threads executed increase () twice, the result was added only once.

Some people say, won't volatile invalidate the cache line? But here thread A reads to thread B also carries on the operation, does not modify the Inc value, so thread B reads, still reads 10.

Others say that if thread B writes 11 back to main memory, won't thread A's cache line be made invalid? However, thread A has already done the read operation, and only when it is found that its cache line is invalid will it read the value of main memory, so thread A can only continue to augment itself.

To sum up, in the context of this compound operation, the function of atomicity can not be maintained. However, in the above example of setting the flag value, volatile can guarantee atomicity because the read / write operations to flag are single-step.

In order to ensure atomicity, we can only rely on the atomic operation class of synchronized,Lock and atomic, that is, the basic data types of self-increment (plus 1 operation), self-subtraction (minus 1 operation), and addition operation (plus a number), subtraction operation (minus one number) are encapsulated to ensure that these operations are atomic operations.

Java theory and practice: the correct use of Volatile variables summarizes the key usage scenarios of volatile

You can only use volatile variables instead of locks in a limited number of cases. For the volatile variable to provide ideal thread safety, both of the following conditions must be met:

Writing to a variable does not depend on the current value.

This variable is not included in invariants with other variables.

In fact, these conditions indicate that these valid values that can be written to volatile variables are independent of the state of any program, including the current state of the variable.

The limitation of the first condition prevents the volatile variable from being used as a thread-safety counter. Although an incremental operation looks like a separate operation, it is actually a combined operation consisting of a sequence of read-modify-write operations that must be performed atomically, and volatile does not provide the necessary atomic properties. To do the right thing, you need to keep the value of x unchanged during the operation, which the volatile variable cannot do. However, if you adjust the value to write only from a single thread, you can ignore the first condition. )

One usage scenario of volatile is the status bit; there are also scenarios where only one thread writes and the other threads read.

4.3 memory semantics of locks

The lock allows the critical section to be mutually exclusive. The memory semantics of lock release-acquisition are very similar to the write-read memory semantics of volatile variables.

When a thread releases the lock, JMM flushes the shared variables in the local memory corresponding to the thread to the main memory.

When a thread acquires a lock, JMM invalidates the local memory setting for that thread, so that the critical section code protected by the monitor must read the shared variables from the main memory.

It is not difficult to find that lock release has the same memory voice as volatile write; lock acquisition has the same memory semantics as volatile read.

The memory semantics of lock release and lock acquisition are summarized below.

Thread A releases a lock, essentially when thread A sends a message to the next thread that will acquire the lock (thread A's modifications to the shared variable).

Thread B acquires a lock, essentially when thread B receives a message sent by a thread that modifies the shared variable before releasing the lock.

Thread A releases the lock, and then thread B acquires the lock, essentially sending a message to thread B through main memory.

4.4 memory semantics of the final domain

Reading and writing to the find field is more like normal variable access than the locks and volatile described earlier.

For the find domain, the compiler and processor follow two reordering rules:

1. Writing to a final field within the constructor and then assigning a reference to the constructed object to a reference variable cannot be reordered.

two。 The application of reading an object containing a finalfield for the first time and then reading the finalfield for the first time cannot be reordered between the two operations

The following is an example to illustrate these two rules:

Public class FinalTest {int ibank / ordinary variable final int j; static FinalExample obj; public FinalExample () {I = 1; j = 2;} public static void writer () {obj = new FinalExample ();} public static void reader () {FinalExample object = obj;// read object reference int a = object.i; int b = object.j;}

Suppose that one thread An executes the writer () method, and then another thread B executes the reader () method. Let's illustrate these two rules through the interaction between the two threads.

The reordering rule for writing final fields forbids reordering writes for final fields outside the constructor. The implementation of this rule includes the following two aspects.

1) JMM forbids the compiler from reordering the writes of the finalfield outside the constructor.

2) the compiler inserts a StoreStore barrier after the finalfield is written and before the constructor return. This barrier prevents the processor from reordering the writes of the finalfield outside the constructor.

Now let's analyze the writer method, which contains only one line of code obj = new FinalTest (); this line of code consists of two steps:

1) construct an object of type FinalTest

2) assign the reference of this object to obj

Assuming that there is no reorder between the read object reference of thread B and the member domain of the read object, the following figure is a possible execution sequence

In the figure above, the operation of writing to the normal field is reordered by the compiler outside the constructor, and the reading thread B mistakenly reads the value of the normal variable I before initialization. On the other hand, the operation of writing the final field is limited to the constructor by the reordering rule of the write final field, and the reading thread B correctly reads the value after the initialization of the finalfield variable.

Writing a reordering rule for the final field ensures that the finalfield of the object is initialized before the object reference is visible to any thread, while normal variables do not have this guarantee. As an example in the above figure, when thread B sees the object obj, it is likely that the obj object has not yet been constructed (the write to the ordinary field I is reordered outside the constructor, and the initial value 1 has not been written to the normal field I)

The reordering rule for the read finalfield is that JMM forbids reordering both the reference to the first read object and the finalfield contained in the first read object in a thread (this rule is for processors only). The compiler adds a LoadLoad barrier before the operation of reading the finalfield.

There is an indirect dependency between the reference of the first read object and the finalfield contained in the object for the first time. Because the compiler adheres to indirect dependencies, the compiler does not reorder these two operations. Most processors also follow indirect dependencies and do not reorder these two operations. However, there are a few processors that allow reordering of operations that have indirect dependencies (such as alpha processors), and this rule is specific to such processors.

In the above example, the reader method contains three operations

1) read reference variable obj for the first time

2) the reference variable points to the normal domain of the object for the first time

3) the reference variable points to the finalfield of the object for the first time

Now assuming that no reordering occurs on write thread An and that the program executes on processors that do not comply with indirect dependencies, the following figure shows a possible execution timing:

In the figure above, the normal domain operations of the read object are reordered by the processor before the read object reference. When reading a normal domain, the field has not yet been written by the write thread, which is an incorrect read operation, and the reordering rule of the read final domain "restricts" the operation of the read object finalfield after the read object reference, which is a correct read operation.

Reading the reordering rules for the finalfield ensures that before reading an object's finalfield, you must read the references to the objects that contain the finalfield. In this sample program, if the reference is not null, then the finalfield of the referenced object must have been initialized by the A thread.

The final field is a reference type. The finalfield we saw above is the basic data type. What if the finalfield is a reference type?

Public class FinalReferenceTest {final int [] arrs;//final quotes static FinalReferenceTest obj; public FinalReferenceTest () {arrs = new int [1]; / / 1 arrs [0] = 1 public static void write0 2} public static void write0 () {/ A thread obj = new FinalReferenceTest (); / / 3} public static void write1 () {/ / thread B obj.arrs [0] = 2 / / 4} public static void reader () {/ / C thread if (objacent thread null) {/ / 5 int temp = obj.arrs [0]; / / 6}

JMM ensures that the reader thread C can at least see the write thread A writing to the member domain of the final reference object in the constructor. That is, C can see that the value of array subscript 0 is at least 1. While thread B writes to array elements, thread C may or may not see it. JMM does not guarantee that writes from thread B are visible to thread C, because there is data competition between thread B and thread C, and the execution result is unpredictable.

If you want to ensure that reader thread C sees the write of array elements by writer thread B, a synchronization primitive (lock or volatile) needs to be used between writer thread B and reader thread C to ensure memory visibility.

As we mentioned earlier, writing a reordering rule for the finalfield ensures that the finalfield of the object to which the reference variable points to is properly initialized in the constructor before the reference variable is visible to any thread. In fact, to achieve this effect, we also need a guarantee: inside the constructor, the reference to the constructed object cannot be seen by other threads, that is, the object reference cannot be "escaped" in the constructor.

Public class FinalReferenceEscapeExample {final int iposition static FinalReferenceEscapeExample obj;public FinalReferenceEscapeExample () {I = 1; / 1 write final domain obj = this; / / 2 this reference here "escape"} public static void writer () {new FinalReferenceEscapeExample ();} public static void reader () {if (obj! = null) {/ / 3 int temp = obj.i; / / 4}

Suppose one thread An executes the writer () method and the other thread B executes the reader () method. Operation 2 here makes the object visible to thread B before the construction is completed. Even if operation 2 here is the last step of the constructor, and operation 2 comes after operation 1 in the program, the thread executing the read () method may not be able to see the initialized value of the finalfield, because there may be a reorder between operation 1 and operation 2.

Why should JSR-133 enhance the semantics of final:

By adding write and read reordering rules to the finalfield, you can provide Java programmers with initialization security: as long as the object is correctly constructed (references to the constructed object do not "escape" in the constructor), you do not need to use synchronization (the use of lock and volatile) to ensure that any thread can see the value of the finalfield after it has been initialized in the constructor.

5. How does JMM deal with the three major characteristics of concurrency?

JMM is built around how to deal with atomicity, visibility and ordering in the concurrency process.

Atomicity:

In Java, the operations of reading and assigning basic data types are atomic operations. The so-called atomic operations mean that these operations are uninterruptible and must be done or not performed. For example:

I = 2b / j = I / I = I + 1

In the above four operations, inotify 2 is a read operation, must be an atomic operation, jroomi you think is an atomic operation, in fact, it is divided into two steps, one is to read the value of I, and then assign a value to j, this is a 2-step operation, not an atomic operation, inotify + and I = I + 1 are actually equivalent, read the value of I, add 1, and then write back to main memory, that is a three-step operation. So in the above example, the final value may occur in a variety of cases, just because it does not satisfy atomicity.

JMM can only guarantee the atomicity of reading / writing to a single volatile variable, but coincidence operations like volatile++ are not atomic, so it is necessary to ensure the atomicity of the whole code with the help of synchronized and Lock. The thread is bound to brush the value of I back to main memory before releasing the lock.

Visibility: visibility means that when one thread modifies the value of a shared variable, other threads are immediately aware of the change. The Java memory model realizes visibility by synchronizing the new value back to the main memory after the variable is modified and refreshing the variable value from the main memory before the variable is read, which depends on the main memory as the transmission medium.

Whether it's a normal variable or a volatile variable, the difference is that volatile's special rules ensure that new values are immediately synchronized to main memory and refreshed from main memory immediately before each use. Because, it can be said that volatile guarantees the visibility of variables in multithreaded operations, while ordinary variables do not.

In addition to volatile, there are two other keywords in java to achieve visibility, namely synchronized and final (variables decorated by final, with the highest level of thread safety). The visibility of synchronous blocks is obtained by the rule that before performing a unlock operation on a variable, the variable must be synchronized back into main memory (performing a store,write operation). The visibility of the final keyword means that once the field modified by final is initialized in the constructor, and the constructor does not pass out the reference to "this" (this reference escape is a very dangerous thing, other threads may access the "initialized half" object through this reference), then the value of the final field can be seen in other threads.

Orderliness: the orderliness of JMM is discussed in detail when explaining volatile. The natural orderliness in java programs can be summed up in one sentence: if you observe in this thread, all operations are ordered; if you observe another thread in one thread, all operations are disordered. The first half of the sentence refers to "serial semantics in the thread", and the second half refers to the phenomenon of "instruction rearrangement" and "synchronous delay between working memory and main memory".

The first half of the sentence can be solved by the as-if-serial semantics stipulated by JMM, and the second half can be solved by the happens-before principle stipulated by JMM. Java semantics provides two keywords, volatile and synchronized, to ensure the order of operations between threads. The volatile keyword itself contains the semantics that forbids instruction rearrangement, while synchronized is obtained by the rule that only one thread is allowed to lock a variable at a time. This rule determines that two synchronous blocks that hold the same lock can only enter serially.

At this point, the study of "detailed explanation of Java memory region and memory model" is over. I hope to be able to solve your doubts. The collocation of theory and practice can better help you learn, go and try it! If you want to continue to learn more related knowledge, please continue to follow the website, the editor will continue to work hard to bring you more practical articles!

Welcome to subscribe "Shulou Technology Information " to get latest news, interesting things and hot topics in the IT industry, and controls the hottest and latest Internet news, technology news and IT industry trends.

Views: 0

*The comments in the above article only represent the author's personal views and do not represent the views and positions of this website. If you have more insights, please feel free to contribute and share.

Share To

Development

Wechat

© 2024 shulou.com SLNews company. All rights reserved.

12
Report