What is the problem with semi-initialization of java objects?

Background: Recently, I accidentally saw the issue of semi-initialization of java objects. I took advantage of the weekend to talk about the issue of semi-initialization of java objects.
Insert image description here

I. Introduction

1. What is Java object semi-initialization?

The creation process of Java objects includes memory allocation, executing construction methods for initialization and setting reference addresses in heap memory. In a multi-threaded environment, because the Java memory model allows reordering of instructions, one thread may see the reference address of an object created by another thread, but this object has not yet been initialized. At this point, we call this object a semi-initialized object.

2. Impact caused by object semi-initialization problem

Object semi-initialization problems may cause abnormal program behavior in a multi-threaded environment, such as:

  • Thread safety issues When a thread uses an object that has not yet been initialized, it may cause data inconsistency or program crash.
  • Visibility issues Multiple threads may see inconsistent states when accessing semi-initialized objects, leading to program logic errors.
  • Memory leaks Semi-initialized objects can cause memory leaks because semi-initialized objects can be mistaken for reachable objects during garbage collection.

2. Detailed explanation of object semi-initialization problem

1. Java object creation process

  • Allocate memory space: Allocate memory space for new objects, usually in the Heap. At this time, the memory addresses assigned to the object are all default values, such as 0, false, null, etc.
  • Initialize the object: Execute the construction method to initialize and complete the state setting of the object. At this stage, the object's property values ​​are set according to the programmer's wishes.
  • Set the reference address in the heap memory: Set the reference address of the object to the variable so that the program can access and operate the object.

2. Causes of object semi-initialization problem

  • Java memory model: The Java memory model allows for the reordering of instructions, that is, the execution order of constructors can be inconsistent with the order in which the code is written. This is to improve processor and compiler execution efficiency. However, in a multi-threaded environment, this optimization may lead to object semi-initialization problems.
  • Problems caused by reordering: Due to instruction reordering, one thread may see the reference address of an object created by another thread, but this object has not yet completed initialization, resulting in object semi-initialization problems. Here is a simple example:
public class Example {
    
    
    private static Example instance;
    private int value;

    private Example() {
    
    
        value = 10;
    }

    public static Example getInstance() {
    
    
        if (instance == null) {
    
    
            instance = new Example();
        }
        return instance;
    }

    public int getValue() {
    
    
        return value;
    }
}

ExampleThe class is a singleton class, and getInstance()the only instance is obtained through the method. Due to instruction reordering, the following execution order is possible:

  1. Allocate memory space
  2. Set the reference address in the heap memory ( notinstance at this time, but the object has not been initialized yet)null
  3. initialize object

In a multi-threaded environment, if thread A is executing getInstance()a method, between steps 2 and 3, thread B also starts executing getInstance()the method. At this time, thread B finds instancethat it is not null, so it returns directly instance, but instancethe object pointed to at this time has not yet been initialized. In this way, thread B will access the semi-initialized object.

To avoid this situation, you can use double-checked locking (Double-Checked Locking) method:

public static Example getInstance() {
    
    
    if (instance == null) {
    
    
        synchronized (Example.class) {
    
    
            if (instance == null) {
    
    
                instance = new Example();
            }
        }
    }
    return instance;
}

However, this approach may still suffer from object semi-initialization issues. In order to completely solve this problem, you need to use volatilekeywords:

private static volatile Example instance;

Using volatilekeywords can prohibit instruction reordering to ensure that the order of objects in allocating memory space, initializing objects, and setting reference addresses in heap memory is not disrupted. This avoids object semi-initialization problems.

3. Example Analysis: Performance of Object Semi-Initialization Problem

1. Semi-initialization in single-threaded environment

In a single-threaded environment, since there are no concurrent operations, object initialization is usually performed in a predetermined order, so half-initialization rarely occurs. However, in some special cases, such as cyclic dependencies or exception handling, the problem of object semi-initialization may still occur.

For example, in the following code, the self-reference in the constructor creates a circular dependency, causing the object to generate a semi-initialized self-reference during the initialization process.

public class Example {
    
    
    private Example self;

    public Example() {
    
    
        // 在对象初始化过程中创建一个自引用
        self = this;
    }

    public void check() {
    
    
        if (self != null) {
    
    
            System.out.println("Object is partially initialized!");
        }
    }
}

public static void main(String[] args) {
    
    
    Example example = new Example();
    example.check();
}

In this example, check()the method prints "Object is partially initialized!" because the selfreferenced object was still being initialized when it was called.

2. Semi-initialization in multi-threaded environment

In a multi-threaded environment, due to the concurrent execution of threads and the reordering of instructions, it is easy to cause the problem of semi-initialization of objects. This problem is even more obvious when using singleton mode or delayed initialization.

For example, in the following code, although double-checked locking is used, other threads may still see a half-initialized singleton object due to instruction reordering.

public class Singleton {
    
    
    private static Singleton instance;
    private SomeObject obj;

    private Singleton() {
    
    
        obj = new SomeObject();
    }

    public static Singleton getInstance() {
    
    
        if (instance == null) {
    
    
            synchronized (Singleton.class) {
    
    
                if (instance == null) {
    
    
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }

    public SomeObject getObj() {
    
    
        return obj;
    }
}

public static void main(String[] args) {
    
    
    // 创建多个线程并发获取单例对象
    for (int i = 0; i < 10; i++) {
    
    
        new Thread(() -> {
    
    
            Singleton singleton = Singleton.getInstance();
            // 检查获取到的单例对象是否初始化完全
            if (singleton.getObj() == null) {
    
    
                System.out.println("Singleton is partially initialized!");
            }
        }).start();
    }
}

Multiple threads executing getInstance()the method concurrently may see a semi-initialized Singletonobject, that is, objthe properties may be null.

4. Solutions and preventive measures

1. Disable instruction reordering

  • Use the final keyword: In a multi-threaded environment, the final keyword can ensure the visibility of the initialization completion and prevent the object from being referenced by other threads before the initialization is completed.
  • Use constructor privatization: Private constructors can force users to create objects in a specific way, thereby preventing users from obtaining a reference to the object without completing initialization.

2. Use the pattern of safely publishing objects

  • Use immutable objects: Once an immutable object is initialized, its state cannot be changed. This ensures that no other thread can see the state of the object during initialization.
  • Use the volatile keyword: The volatile keyword can ensure the visibility of variables in a multi-threaded environment and prohibit instruction reordering, thereby preventing the problem of object semi-initialization.
  1. In actual programming, you should try to avoid referencing yourself or exposing your own references in the constructor to prevent problems caused by escape.

  2. For singleton objects, it can be implemented through internal classes, which can ensure thread safety and the uniqueness of singleton objects.

For example:

public class Singleton {
    
    
    private Singleton() {
    
    
        // do something
    }

    private static class SingletonHolder {
    
    
        private static final Singleton INSTANCE = new Singleton();
    }

    public static Singleton getInstance() {
    
    
        return SingletonHolder.INSTANCE;
    }
}

The initialization of Singleton is deferred until the SingletonHolder class is actually loaded, and the JVM ensures thread safety and singleness.

5. Reference documents

  1. 《Java Memory Model》https://docs.oracle.com/javase/specs/jls/se8/html/jls-17.html
  2. Java Tutorials - Concurrency.https://docs.oracle.com/javase/tutorial/essential/concurrency/
  3. "java internal model" https://www.cnblogs.com/nexiyi/p/java_memory_model_and_thread.html

おすすめ

転載: blog.csdn.net/wangshuai6707/article/details/132989866