Null pointer problem due to instruction rearrangement of instantiated objects in multithreaded case

  • The question comes from: Why can't there be less volatile keywords in the double check singleton mode?
public class Singleton {
    private static Singleton instance = null;
    private Singleton() { }
    public static volatile Singleton getInstance() {
        if(instance == null) {
            synchronzied(Singleton.class) {
                if(instance == null) {
                    instance = new Singleton();  //Nonatomic operation
                }
            }
        }
        return instance;
    }
}

[External chain picture transfer failed, source station may have anti-theft chain mechanism, it is recommended to save the picture and upload it directly (img-6wQwGN4p-16317620422) (C:Users17575AppDataRoamingTyporatypora-user-imagesimage-20286.png)]

  • From the Byte Code Directive Perspective
 	getstatic 		'demo4/Singleton.instance','Lcom/demo4/Singleton;'
    ifnonnull l5 
        //Request memory space, create objects
    _new '/demo4/Singleton'
        //Copy the top value of the stack and push it to the top
    dup 
        //Call instance methods that require special handling (this directive may be rearranged back to the following putstatic directive)
    INVOKESPECIAL com/itheima/pattern/singleton/demo4/Singleton.<init> ()V
        //Assign a value to the static field of the specified class
    putstatic '/demo4/Singleton.instance','Lcom/demo4/Singleton;'
   l5
    aload 0
    monitorexit
  • From the bottom of calling C language
memory = allocate();       //1: Allocate memory space for objects 
ctorInstance(memory);     //2: Initialize object 
instance = memory;        //3: Set instance s to point to the memory address just allocated

For a JVM, when a class is created, the result is the same for operation 2 and 3, so the JVM can optimize the reordering of instructions against them.

  • After reordering, the following
memory = allocate();     //1: Allocate memory space for objects 
instance = memory;       //3:instance points to the memory address just allocated, at which point the object has not been initialized
ctorInstance(memory);    //2: Initialize object

In a single-threaded scenario, of course, this is OK; but in a multithreaded scenario, imagine this: Thread A executes this assignment statement (operation 2) and assigns the assignment object to the instance reference before it is initialized, at which point exactly another thread, B, enters the method, determines that the reference to the instance is null, and then returns it to use, causing an error.

  • Solution
volatile Keyword guarantees variable visibility because of volatile All operations are in Main Memory(In main memory, while Main Memory Shared by all threads at the expense of performance, register or Cache(Level 3 caches), because they are not global, do not guarantee visibility, and can produce dirty reads.

volatile Another effect is to partially prevent reordering (in JDK1.5 After that, you can use volatile Variables prohibit instruction reordering), for volatile The operation instructions for variables will not be reordered, because reordering can also cause visibility problems.

In terms of visibility, locks (including explicit locks, object locks) and read-write of atomic variables ensure the visibility of variables.

However, there are slightly different implementations, such as synchronous locks that guarantee re-reading the data refresh cache from memory when a lock is acquired, and write data back to memory when a lock is released to keep the data visible. volatile Variables are simply read-write memory.

The volatile keyword prevents instructions from being reordered by providing a **"memory barrier"**. To achieve volatile memory semantics, the compiler inserts a memory barrier into the instruction sequence to prevent certain types of processor reordering when generating byte code.

Most processors support memory barrier Memory Barrier (Memory Fence) instructions.

It is almost impossible for the compiler to find an optimal layout to minimize the total number of insert barriers, so the Java memory model takes a conservative strategy. The following is a JMM memory barrier insert strategy based on conservative strategies:

  • Insert a write barrier after each volatile write operation, before which changes to shared variables are synchronized to main memory to ensure visibility and order of the program before the write barrier.

  • Insert a read barrier before each volatile read operation, after which the read of shared variables loads the latest data in main memory, ensuring the visibility and orderliness of the program behind the read barrier.


Meaning of instruction rearrangement: Implementing instruction-level parallelism through reordering and grouping at each stage of an instruction without changing the result of the program to improve efficiency.

Modern processors are designed to complete a CPU instruction with the longest execution time in a clock cycle. Why? It is conceivable that instructions can also be divided into smaller phases. For example, each instruction can be divided into five phases: fetch-instruction decoding-execution-instruction-memory access-data write back.

Tags: Java

Posted on Mon, 20 Sep 2021 06:57:54 -0400 by olm475