在java程序中,有时候需要推迟一些高开销的对象初始化操作,并且只有在使用这些对象时才进行初始化。
但是正确实现线程安全的延迟初始化需要一些技巧,否则容易出现问题。
public class UnsafeLazyInitialization{
private static UnsafeLazyInitialization instance;
public static UnsafeLazyInitialization getInstance(){
if(instance == null) //1:A线程执行
instance = new UnsafeLazyInitialization(); //2:B线程执行
}
}
线程A执行到代码1时,B线程执行代码2,线程A可能看到instance引用的对象还没有完成初始化。
public class UnsafeLazyInitialization{
private static UnsafeLazyInitialization instance;
public synchronized static UnsafeLazyInitialization getInstance(){
if(instance == null) //1:A线程执行
instance = new UnsafeLazyInitialization(); //2:B线程执行
}
}
getInstance()方法做了同步处理,synchronized将导致性能开销。如果getInstance()方法被多个线程频繁调用,导致执行性能下降。反之,延迟初始化方案能提供满意的性能。
双重检查锁定,实现延迟初始化,降低同步的开销
public class DoubleCheckedLocking{ //1
private static DoubleCheckedLocking instance; //2
public static DoubleCheckedLocking getInstance(){ //3
if(instance == null){ //4:第一次检查
synchronized(DoubleCheckedLocking.class){//5:加锁
if(instance == null) //6:第二次检查
instance = new DoubleCheckedLocking(); //7:问题的根源所在
}
}
return instance;
}
}
在线程指定到第4行时,代码读取到instance不为null时,instance引用的对象有可能还没有完成初始化。
memory = allocate(); //1:分配对象的内存空间
ctorInstance(memory); //2:初始化对象
instance = memory; //3:设置instance指向刚分配的内存地址
上述伪代码的2和3可能被重排序,造成延迟初始化问题。
解决办法:1)不允许2和3重排序
2)允许2和3重排序,但不允许其他线程"看到"这个重排序
1)不允许重排序
public class DoubleCheckedLocking{ //1
private volatile static DoubleCheckedLocking instance; //2
public static DoubleCheckedLocking getInstance(){ //3
if(instance == null){ //4:第一次检查
synchronized(DoubleCheckedLocking.class){//5:加锁
if(instance == null) //6:第二次检查
instance = new DoubleCheckedLocking(); //7:
}
}
return instance;
}
}
volatile需要jdk5或更高版本。多线程环境时,加了volatile将禁止对象初始化和分配内存地址的重排序
2)基于类初始化的解决方案
public class InstanceFactory{
private static class InstanceHolder{
public static InstanceHolder instance = new InstanceHolder();
}
public static InstanceHolder getInstance(){
return IntanceHolder.instance; //InstanceHolder类被初始化
}
}
类的初始化,对于每一个类或接口,都有一个唯一的初始化锁LC与之对于。保证多线程情况下,类只会被一个线程进行初始化。
总结:如果确实需要对实例字段使用线程安全的延迟初始化,建议使用volatile的延迟初始化方案。如果是需要对静态字段使用线程安全的延迟初始化,建议使用类初始化方案。
但是正确实现线程安全的延迟初始化需要一些技巧,否则容易出现问题。
public class UnsafeLazyInitialization{
private static UnsafeLazyInitialization instance;
public static UnsafeLazyInitialization getInstance(){
if(instance == null) //1:A线程执行
instance = new UnsafeLazyInitialization(); //2:B线程执行
}
}
线程A执行到代码1时,B线程执行代码2,线程A可能看到instance引用的对象还没有完成初始化。
public class UnsafeLazyInitialization{
private static UnsafeLazyInitialization instance;
public synchronized static UnsafeLazyInitialization getInstance(){
if(instance == null) //1:A线程执行
instance = new UnsafeLazyInitialization(); //2:B线程执行
}
}
getInstance()方法做了同步处理,synchronized将导致性能开销。如果getInstance()方法被多个线程频繁调用,导致执行性能下降。反之,延迟初始化方案能提供满意的性能。
双重检查锁定,实现延迟初始化,降低同步的开销
public class DoubleCheckedLocking{ //1
private static DoubleCheckedLocking instance; //2
public static DoubleCheckedLocking getInstance(){ //3
if(instance == null){ //4:第一次检查
synchronized(DoubleCheckedLocking.class){//5:加锁
if(instance == null) //6:第二次检查
instance = new DoubleCheckedLocking(); //7:问题的根源所在
}
}
return instance;
}
}
在线程指定到第4行时,代码读取到instance不为null时,instance引用的对象有可能还没有完成初始化。
memory = allocate(); //1:分配对象的内存空间
ctorInstance(memory); //2:初始化对象
instance = memory; //3:设置instance指向刚分配的内存地址
上述伪代码的2和3可能被重排序,造成延迟初始化问题。
解决办法:1)不允许2和3重排序
2)允许2和3重排序,但不允许其他线程"看到"这个重排序
1)不允许重排序
public class DoubleCheckedLocking{ //1
private volatile static DoubleCheckedLocking instance; //2
public static DoubleCheckedLocking getInstance(){ //3
if(instance == null){ //4:第一次检查
synchronized(DoubleCheckedLocking.class){//5:加锁
if(instance == null) //6:第二次检查
instance = new DoubleCheckedLocking(); //7:
}
}
return instance;
}
}
volatile需要jdk5或更高版本。多线程环境时,加了volatile将禁止对象初始化和分配内存地址的重排序
2)基于类初始化的解决方案
public class InstanceFactory{
private static class InstanceHolder{
public static InstanceHolder instance = new InstanceHolder();
}
public static InstanceHolder getInstance(){
return IntanceHolder.instance; //InstanceHolder类被初始化
}
}
类的初始化,对于每一个类或接口,都有一个唯一的初始化锁LC与之对于。保证多线程情况下,类只会被一个线程进行初始化。
总结:如果确实需要对实例字段使用线程安全的延迟初始化,建议使用volatile的延迟初始化方案。如果是需要对静态字段使用线程安全的延迟初始化,建议使用类初始化方案。