背景: 最近无意间看到关于java对象半初始化问题,趁着周末今天聊聊java对象半初始化问题。
Java对象的创建过程包括内存分配、执行构造方法进行初始化和设置堆内存中的引用地址。在多线程环境下,由于Java内存模型允许指令的重排序,可能导致一个线程看到了另一个线程创建的对象的引用地址,但是这个对象还没有完成初始化。此时,我们称这个对象为半初始化对象。
对象半初始化问题可能导致多线程环境下程序的行为异常,比如:
- 线程安全问题:一个线程在使用尚未初始化完成的对象时可能导致数据不一致或程序崩溃。
- 可见性问题:多个线程在访问半初始化对象时可能看到不一致的状态,导致程序逻辑错误。
- 内存泄漏:半初始化对象可能导致内存泄漏,因为在垃圾回收时,半初始化对象可能被误认为是可达对象。
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;
}
}
Example
类是一个单例类,通过 getInstance()
方法获取唯一的实例。由于指令重排序,可能出现以下执行顺序:
instance
不为 null
了,但对象还没有完成初始化)在多线程环境下,如果线程A在执行getInstance()
方法时,在第2步和第3步之间,线程B也开始执行getInstance()
方法。此时,线程B发现instance
不为 null
,于是直接返回instance
,但此时instance
指向的对象尚未完成初始化。这样,线程B就会访问半初始化的对象。
为了避免这种情况,可以使用双重检查锁定(Double-Checked Locking)的方法:
public static Example getInstance() {
if (instance == null) {
synchronized (Example.class) {
if (instance == null) {
instance = new Example();
}
}
}
return instance;
}
但是,这种方法仍然可能会遇到对象半初始化问题。为了彻底解决该问题,需要使用volatile
关键字:
private static volatile Example instance;
使用volatile
关键字可以禁止指令重排序,确保对象在分配内存空间、初始化对象和设置堆内存中的引用地址的顺序不被打乱。这样就可以避免对象半初始化问题。
在单线程环境下,由于没有并发操作,对象的初始化通常是按照预定的顺序进行的,因此很少出现半初始化的情况。但是,在某些特殊情况下,例如循环依赖或者异常处理中,仍然可能出现对象半初始化的问题。
例如,以下的代码中,由于构造函数中的自引用创建了一个循环依赖,导致对象在初始化过程中产生了一个半初始化的自引用。
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();
}
在这个例子中,check()
方法会打印出 “Object is partially initialized!”,因为当它被调用时,self
引用的对象还处在初始化过程中。
在多线程环境下,由于线程的并发执行和指令的重排序,很容易导致对象半初始化的问题。尤其是在使用单例模式或者延迟初始化的时候,这种问题更加明显。
例如,以下的代码中,尽管使用了双重检查锁定,但是由于指令重排序,其他线程仍然可能看到一个半初始化的单例对象。
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();
}
}
多个线程并发执行 getInstance()
方法,可能会看到一个半初始化的 Singleton
对象,即 obj
属性可能为 null
。
在实际编程中,应尽量避免在构造函数中引用自身或暴露自身的引用,防止逸出引起的问题。
对于单例对象,可以通过内部类的方式来实现,既能保证线程安全,又能保证单例对象的唯一性。
例如:
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;
}
}
Singleton 的初始化被推迟到了 SingletonHolder 类被真正的加载时才执行,同时由 JVM 来保证线程安全和单一性。