This article has been synchronized to the GitHub open source project: JVM Underlying Principles Parsing

Java Memory Model

The JVM virtual machine specification has attempted to define a Java memory model to mask the memory access differences of various hardware and operating systems, so that Java programs can achieve consistent memory access across a variety of platforms.

However, it is not easy to define such a memory model, which must be rigorous enough to make Java’s concurrent memory access operations unambiguous. But it must be loose enough so that the implementation of the virtual machine has the freedom to take advantage of the various hardware. After a long period of verification and remediation, the Java memory model finally came of age after JDK1.5, which implemented the JSR133 specification.

Main memory and working memory

The Java Memory model specifies that all variables are stored in Main Memory, and that each thread has its own Work Memory.

  • The working memory holds a main memory copy of the variables used by the thread,
  • Thread reads and writes to variables must be performed in working memory.
  • Instead of directly accessing main memory data.
  • Different threads cannot read or write to each other’s working memory, and variable transfers between threads must be passed through main memory.

Interaction between main memory and working memory

The Java memory model defines the following eight operations (each operation isThe atomic.Thou shalt not points)

  • The lock lock: acts on main memory to identify a variable as thread-exclusive
  • Unlock: unlock: acts on main memory to free a variable that is occupied by a thread
  • Read read: Reads data from the main memory to the working memory to facilitate subsequent load operations
  • The load is loaded: Puts the variables obtained by the read operation from main memory into the working memory copy of the variables
  • Use use: Passes variables in working memory to the execution engine. This operation is performed when a virtual opportunity comes to a bytecode that needs to use the value of a variable
  • Assign the assignment: a variable that assigns a value from the execution engine to the working memory. This operation is performed when the virtual opportunity goes to an assignment operation
  • Store to store: Passes the value of the working memory to the main memory for subsequent write operations
  • Write to write: Writes variables obtained from working memory in the store operation to main memory

For example:

  • If you want to copy a variable from main memory to working memory, you perform read and load in sequence
  • To write a variable from working memory to main memory, the store and write operations are performed in turn

The preceding eight operations must meet the following rules:

  • One of the read and load, store, and write operations is not allowed separately. That is, a variable is not allowed to be read from main memory but not accepted by working memory, and it is not allowed to be written back from working memory but not accepted by main memory.
  • A thread is not allowed to discard its most recent assign operation, that is, variables 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 for no reason (no assign operation has occurred).
  • A new variable can only be created in main memory. It is not allowed to use an uninitialized (load or assign) variable in working memory. The use and store operations must be performed before the assign and load operations can be performed.
  • Only one thread can lock a variable at a time, but the lock operation can be repeated by the same thread many times. After multiple lock operations, the variable can be unlocked only by performing the unlock operation for the same number of times. Lock and unlock must be paired
  • If you lock a variable, the value of the variable will be cleared from the working memory, and you will need to load or assign the variable again to initialize it before the execution engine can use it
  • Unlock is not allowed if a variable has not been locked by the lock operation. It is also not allowed to unlock a variable that is locked by another thread.
  • Before you can unlock a variable, you must synchronize it to main memory (store and write).

Special rules for Volatile

Volatile is arguably the lightest synchronization mechanism provided by the Java Virtual Machine. But it’s not easy to understand correctly and completely.

Specified in the Java memory model

When a variable is defined as volatile, it means that the thread’s working memory is invalid. All reads and writes to the value are applied directly to main memory.

So it has immediate visibility to all threads.

  1. Ensure that this variable is valid for all threadsImmediate visibility

    When the value of a variable is modified, the new value is immediately known to other threads. Ordinary variables do not do this because the value of ordinary variables is passed through main memory between threads. For example, when thread A writes back to A variable, thread B does not see the new value until after thread A writes back, when thread B operates on main memory. While A is writing back to main memory, B is still reading the old value.

    However, it does not follow that operations based on volatile variables are safe under concurrency, because operators in Java are not atomic. This makes volatile variables unsafe to operate concurrently.

    Code that verifies that volatile variables are not safe to operate concurrently

    First we create 20 threads, each of which increments the volatile variable 1000 times.

    / * * * @ author: writing Bug komori * @ [email protected] 】 【 time: 2021/07/31 * @ description: through code validation 【 volatile variables under the concurrent operation is unsafe * /
    public class VolatileTest {
    
        // Count modified by volatile
        private static volatile int count = 0;
    
        // Count increment method
        public static void increment(a){
            count++;
        }
    
        public static void main(String[] args) {
    
            // A runnable interface that increments count 1000 times
            Runnable runnable = new Runnable() {
                @Override
                public void run(a) {
                    System.out.println(Thread.currentThread().getName() + "The thread starts incrementing the count.");
                    for (int i = 0; i < 1000; i++) {
                        increment();
                    }
                    System.out.println(Thread.currentThread().getName() + "Thread incrementing count ends."); }};// Create 20 threads and start
            for (int i = 0; i < 20; i++) {
    
                Thread thread = new Thread(runnable);
                thread.setName((i+1) + "Thread #");
                thread.start();
    
            }
    
            while (Thread.activeCount() > 2) {// Return the main thread to the ready state
                Thread.yield();
            }
    
            System.out.println("All threads end,count ="+ count); }}Copy the code

    If the program is safe for concurrency, then the count value must end up being 20*1000 = 20000; That is, volatile variables are safe to operate concurrently if the result is 20000

    By running the program several times, we find that count is always less than 20000.

    So why?

    We decompile the above code and then analyze the bytecode instructions for the Increment method.

    0 getstatic #2 <cn/shaoxiongdu/chapter6/VolatileTest.count : I>
    3 iconst_1
    4 iadd
    5 putstatic #2 <cn/shaoxiongdu/chapter6/VolatileTest.count : I>
    8 return
    Copy the code

    As you can see, one line of count++ code is divided into four lines of bytecode file to execute. Through our bytecode analysis, we found that

    When the offset 0 bytecode getStatic takes the count from the local variable to the top of the operand stack, volatile guarantees that the count is correct, but when the iconst_1 or iadd operations are performed, other threads have changed the count. In this case, The count at the top of the operand stack is expired data, so the putStatic bytecode instruction makes it possible to synchronize smaller values into main memory. So the final value is going to be a little bit less than 20,000.

    That is, volatile variables are not safe to operate concurrently.

    In a concurrent environment, volatile variables are only immediately visible to all threads, and if a write is to be performed, it must be locked.

This article has been synchronized to the GitHub open source project: JVM Underlying Principles Parsing

Copy the code