单例模式
Java多线程程序中,有时候需要延迟对象的初始化,降低初始化类和创建对象的开销。
而单例模式就提供了这种解决方法。在单例模式中,只允许一个类创建一个对象。所以不能让外部提供构造方法创建它,于是就
private SingleDemo(){}
且只提供一个对象,则用一个公共方法来获取这一个对象。
单例模式有饿汉式,和懒汉式。
饿汉式,缺点是提前就进行了对象初始化,资源的浪费,等到需要的时候再初始化,要延迟初始化。但是这也不是一个缺点,如果这个实例初始化耗时很长,我们可以再系统上线前就知道,不用等到系统允许一段时间才出现问题,类似fail-fast。
public class SingletonDemo2 {
private SingletonDemo2(){}
private static SingletonDemo2 sd = new SingletonDemo2();
public static SingletonDemo2 getInstance(){
return sd;
}
}
懒汉式,延迟加载。但是很明显,这里完全没有进行同步,所以会并发问题。
public class SingletonDemo2 {
private SingletonDemo2(){}
private static SingletonDemo2 sd;
public static SingletonDemo2 getInstance(){
if (sd == null) {//1
sd = new SingletonDemo2();//2
}
return sd;
}
}
线程A 执行到注释2代码,此时还需创建对象,并不是立即就创建好。那么线程B也可以进入if。
我们可以加锁,对方法加个锁
public class SingletonDemo2 {
private SingletonDemo2(){}
private static SingletonDemo2 sd;
public synchronized static SingletonDemo2 getInstance(){
if (sd == null) {//1
sd = new SingletonDemo2();//2
}
return sd;
}
}
经过加锁之后,确保只有一个线程可以进行对象的初始化。这在竞争不激烈的时候,实现了我们的需求。但是一旦getInstance()方法被频繁调用,那么将会阻塞全部线程在外面然后进入,无论是有没有初始化完毕。太耗费性能。于是,就有了双重检查锁定
public class SingletonDemo2 {
private SingletonDemo2(){}
private static SingletonDemo2 sd;
public static SingletonDemo2 getInstance(){
if (sd == null) {//1
synchronized(SingletonDemo2.class){
if(sd == null){
sd = new SingletonDemo2();//2
}
}
}
return sd;
}
}
这样我们从表面上看代码似乎完成了我们的需求
- 多个线程试图同一时刻初始化对象,确保只会有一个对象去初始化。
- 在对象创建好之后,直接判断,不会阻塞线程。
但是,当你了解了对象的初始化,就会发现有问题。
对象的初始化分为三步
1. memory = allocate() //为对象分配内存
2. ctorInstance(memory) //初始化对象
3. sd = memory //将sd引用指向刚分配的内存地址
2和3之间可能会发生重排序,于是
1. memory = allocate() //为对象分配内存 1
3. sd = memory //将sd引用指向刚分配的内存地址 2
2. ctorInstance(memory) //初始化对象 3
假设此时线程A执行到了 现在的第二步,也就是sd = new SingletonDemo2(); 这意味着线程A执行完了,会退出锁。但是此时还未及时执行第三步 初始化对象的时候,线程B进入了同步代码块。之前一直在阻塞等待锁。进入了之后,即线程B访问到了一个未初始化的对象sd。
发现了问题,怎么解决?
-
禁止2,3重排序,使用volatile
private volatile static AtomicInteger sd; public static AtomicInteger getInstance(){ if (sd == null) { synchronized (SingletonDemo2.class){ if(sd == null){ sd = new AtomicInteger(0); } } } return sd; }
-
让2,3重排序对其它线程不可见,使用静态内部类
private static class Inner{ private static final SingletonDemo2 sd = new SingletonDemo2(); } public static SingletonDemo2 getInstance(){ return Inner.sd; }
JVM的类在初始化阶段(即在Class被加载,且未被线程使用之前),会执行类的初始化。在执行期间,JVM会获取一个锁,这保证多个线程对类初始化是线程安全的。
而有如下代码就会引起类T的初始化
1. T是一个类,而且T类型的实例被创建 2. T是一个类,T中声明的一个方法被调用 3. T声明的一个静态变量被赋值 4. T声明的一个静态字段被使用,且不是一个final修饰的字段
则静态内部类的调用就,是属于4的,静态字段被使用。会引起类的初始化。也是此时,JVM保证只有一个线程会获取锁然后初始化类。
学习自:并发编程的艺术,第三章