Write this article because a byte interview, asked about the Java memory model? I answered some questions about heaps, method areas, virtual machine stacks, etc. And then say it’s not. I look so blind… And then when I learned about JMM, I realized how stupid I was. It was these things. It was called JMM.

So, now write an article to summarize.

1. Concept of Java memory model

Everyone knows that Java runs cross-platform through the Java Virtual machine. But how does it work? Are there any rules?

A: Different computer operating systems operate on memory models differently, so there needs to be a unified specification to do so. So you have to go through the JAVA Memory Model (JMM)

  • It is a JAVA virtual machine specification.
  • It shields memory access differences between hardware and operating systems so that Java programs can achieve consistent memory access across all platforms.

!!!!!!!!! The following paragraphs 2,3,4,5, and 6 are all related specifications or rules for the JAVA memory model. !!!!!!!!!

The article concludes at the end.

2. Memory specification: main memory and working memory

The main purpose of the Java memory model is to define access rules for various variables in a program

  • Focus on the low-level details of storing and fetching variable values from memory in the virtual machine.

The goal of this section is to address variables that can be shared between threads.

Variables are divided into thread-private and thread-public depending on whether they can be shared.

  • Thread private: local variables and method parameters
  • Thread public: instance fields, static fields, and elements that make up array objects

The Java memory model specifies that all variables are stored in main memory. Each thread also has its own working memory, which stores the main memory copy of variables used by the thread. All operations on variables must be carried out in the working memory of the thread, instead of reading or writing data directly from the main memory. As shown in the figure:

This section is distinguished from the JAVA memory area.

  • Memory areas are divided into: virtual machine stack, local method stack, program counters (highlighted in bold as isolated between threads), method area, heap area, and direct memory

3. Memory operations

The specific protocol of interaction between main memory and working memory,

  • Implementation details such as how a variable is copied from main memory to working memory and synchronized from working memory back to main memory.

The following eight operations are defined in the Java memory model to accomplish this. Java virtual machine implementations must ensure that each of the operations described below is atomic and non-separable. (Just a quick look)

  • Lock: A variable acting on main memory that identifies a variable as a thread-exclusive state.
  • Unlock: A variable that acts on main memory. It releases a locked variable so that it can be locked by another thread.
  • Read: A variable acting on main memory that transfers the value of a variable from main memory to the thread’s working memory for subsequent load action.
  • Load: Variable acting on working memory, which puts the value of the variable obtained from main memory by the read operation into a copy of the variable in working memory.
  • Use: variable applied to working memory, which passes 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 needs to use the value of the variable.
  • 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 places the value of a variable in main memory obtained from the store operation in working memory.

The Java design team simplified the operations of the Java memory model into four types: read, write, Lock, and unlock.

4. Rules for volatile variables

Variables defined by volatile have two properties:

  • Ensure that this variable is visible to all threads
  • Disallow instruction reordering optimization

4.1 the visibility

1. Concept: When one thread changes the value of this variable, the new value is immediately visible to other threads. This is not the case with ordinary variables, whose values are passed from thread to thread through main memory.

  • Common variable writing and reading process: Thread A modifies the value of A common variable and writes back to the main memory. Another thread B reads back to the main memory after thread A finishes writing back, and the new variable value is visible to thread B.

2. Thread safety:

In concurrency, it is not necessarily thread-safe.

Java operations (a=b+1, not a=1) are not atomic operations, which makes volatile variables unsafe concurrently.

There are a lot of threads on the web that add 10000 times to a variable, but the end result is not 10000* threads

The ++ operation of a variable is broken down into three parts in the bytecode, which makes it thread unsafe (separate reads and writes are safe).

  • Read the value
  • Change the value
  • Store value

4.2 Disabling command reordering

1. Concept: it refers to the adoption of the processor to allow multiple instructions to be separately developed and sent to each corresponding circuit unit for processing in a non-programmed order. But it doesn’t mean that the instructions are arbitrarily rearranged, the processor has to be able to handle the instruction dependencies correctly to ensure that the program gets the correct result.

Note: instruction reordering is not perceptible during the execution of a method on the same thread, but in fact some of the execution order is changed but the result remains the same.

  • Because the Java memory model defines “semantics that appear serial in threads.”

2. Example of disallowing instruction reordering: singleton slacker

class Singleton{
  private static volatile Singleton instance = null;
  private Singleton(a){}
  public static Singleton getInstance(a){
    if(instance == null) {synchronized(Singleton.class){
        if(instance == null){
          instance = newSingleton(); }}}returninstance; }}Copy the code

If there were no volatile modifier, the instance would have been allocated memory at the time of the new Singleton, but there was nothing in memory. At this point, another thread gets the singleton to use, and finds that there are no objects in the memory that cannot be used (i.e. half initialized), and a thread-safety problem occurs.

Lock addL $0x0,(% ESP) this assembly statement add LOCK modifier.

  • It writes the local processor’s cache to memory, which can cause other processors or other kernels to invalidate their cache.
  • It acts as a Memory Barrier (or Memory Fence, not to be confused with the Barrier used by the garbage collector to capture variable accesses described in Chapter 3) because it writes the CPU cache back to MemorylockThe previous instructions have to be done and inlockThis is the barrier before the operation after the instruction.

3. Principles of use:

  • Volatile reads have the same performance cost as normal variables, but writes can be slower because they require many memory-barrier instructions to be inserted into the native code to keep the processor from executing out of order. Even so, the overall cost of volatile is lower than locking in most scenarios.
  • The only basis for our choice between volatile and locking is whether the semantics of volatile meet the requirements of the usage scenario.

4.3 summarize

  1. In working memory, each time V is used, the latest value must be refreshed from main memory to ensure that other threads’ changes to V are visible.
  2. In working memory, each change to V must be immediately synchronized back to main memory to ensure that other threads can see their changes to V.
  3. Variables requiring volatile modifications are not optimized by instruction reordering, ensuring that the code executes in the same order as the program.

5. Special rules for long and double

The above Java memory model requires that lock, unlock, read, load, assign, use, Store, and write operations be atomic.

However, for 64-bit data types (long and double), the model has a very loose rule that allows the virtual machine to write and write 64-bit data that is not volatile into two 32-bit operations.

  • Allow virtual machine implementations to choose whether or not to guarantee atomicity for load, store, read, and write operations on 64-bit data types. This is called the “non-atomic protocol for longs and doubles.”

Through practical tests, non-atomic access behavior does not occur in the current mainstream commercial 64-bit Java VIRTUAL machine, but for 32-bit Java virtual machine, such as HotSpot VIRTUAL machine on the more common 32-bit x86 platform, there is a risk of non-atomic access to long data.

  • You generally don’t need to write code that specifically declares long and double as volatile for this reason.

6. Happens-before principle

If all the ordering in the Java memory model was done by volatile and synchronized alone, a lot of operations would be very verbose, but we don’t realize this when we write Java concurrent code. This is because the Java language has a happens-before principle.

There are some natural antecedents in the Java memory model that already exist without the assistance of any synchronizer and can be used directly in coding. There are 8 rules:

  1. Program Order Rule: In a thread, actions written earlier take place before those written later, in Order of control flow. Note that we are talking about the control flow sequence, not the program code sequence, because we have branches, loops, and so on to consider.
  2. Monitor Lock Rule: An UNLOCK operation occurs first when a subsequent Lock operation is performed on the same Lock. What must be emphasized here is “the same lock”, and “behind” refers to the sequence of time.
  3. Volatile Variable Rule: Writes to a volatile Variable occur first and then reads occur later, again in chronological order.
  4. Thread Start Rule: The Start () method of a Thread object precedes every action of the Thread.
  5. Thread Termination Rule: All operations ina Thread occur in the Termination detection of this Thread first. We can check whether the Thread has terminated by means of whether the Thread:: Join () method ends or the return value of Thread::isAlive(), etc.
  6. Interruption Rule: Interrupt () calls to the interrupt() method occur when the interrupted Thread code detects that the Interruption has occurred. Thread:: Interrupted () checks whether the Interruption has occurred.
  7. Finalizer Rule: An object’s initialization completes (the end of constructor execution) before its Finalize () method.
  8. Transitivity: If operation A precedes operation B and operation B precedes operation C, operation A precedes operation C.

7. To summarize

7.1 atomic

Atomic variable operations directly guaranteed by the Java memory model include read, load, assign, use, Store, and write. It can be roughly considered that access, read, and write of basic data types are atomic.

  • The exception is the nonatomic agreement between long and double, and the reader needs only to know about it and not to worry too much about exceptions that almost never happen.

If application scenarios require a broader atomicity guarantee, the Java memory model also provides the synchronized keyword, so that operations between synchronized blocks are atomicity.

  • Through lock and UNLOCK operations, the VM does not open lock and UNLOCK operations directly to users, but provides higher-level bytecode instructions Monitorenter and Monitorexit to implicitly use these two operations.

7.2 the visibility

Visibility means that when one thread changes the value of a shared variable, other threads are immediately aware of the change.

  • The visibility of volatile is that special rules for volatile ensure that new values are immediately synchronized to main memory and flushed from main memory immediately before each use. Thus we can say that volatile guarantees visibility of variables in multithreaded operations, whereas normal variables do not.
  • The visibility of synchronized blocks is gained by the rule that a variable must be synchronized back to main memory (store, write) before unlock is performed.
  • Visibility of the final keyword means: Once a field modified by final is initialized in the constructor and the constructor does not pass a reference to “this” (this reference escape is a dangerous thing because other threads may access the “half-initialized” object through this reference), the value of the final field is visible in other threads.

7.3 order

The natural orderliness in Java programs can be summarized as follows:

  • If viewed within this thread, all operations are in order;
  • If you observe another thread in one thread, all operations are out of order.

The first part of the sentence refers to the semantics that appear to be serial in the thread, the second part refers to the instruction reordering phenomenon and the synchronization delay between the working memory and the main memory.

  • The volatile keyword itself contains semantics that prohibit instruction reordering.
  • Synchronized is acquired by the rule that a variable can only be locked by one thread at a time. This rule determines that two synchronized blocks holding the same lock can enter only serially.

Reference for this article: Understanding the Java Virtual Machine in Depth (version 3)