单例模式中的double check

单例模式是设计模式中最简单的模式了,它的目的是为了保证一个流程中只有一个对象存在,相当于一个全局变量。

1 单例模式的实现

创建一个类,调用者不能通过默认构造方法的方式创建实例,而是提供一个接口用来返回唯一的实例。

public class SingleInstance{
    private static SingleInstance instance = null;
    private SingleInstance(){}
    public static SingleInstance getInstance(){
        if(instance == null){
            instance = new SingleInstance();
        }
        return instance;
    }
}

2 同步单例模式的实现

1中是最简单的单例模式,但是在多线程状态下就出现问题了,如果线程A调用getInstance()方法时,开始先判断instance是否为空,如果为空则需要创建实例,但是此时A线程阻塞,B线程也调用了getInstance()方法,也发现Instance为空,所以需要创建实例,此时A线程继续执行,则又创建了一个实例,A和B线程各创建了一个实例对象,违背了单例模式的初衷,所以需要实现单例模式的线程同步。

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

3 提高单例模式的性能

2中的单例模式保证了线程的同步问题,但是也存在着问题,那就是性能问题,采用synchronized关键字来回对方法加锁有很大的性能开销,那么能否只对某个对象加锁呢,答案是可以的。去掉方法同步的关键字synchronized,对SingleInstance.class进行加锁操作,代码可如下:

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

接下来的问题是当多个线程同时获取实例时,依然要对某个对象进行同步操作,有没有更好的办法呢,答案是有的,那就是在同步之前先判断Instance是否为空,如果已经是非空了,也就没必要同步了,那么可以改成如下的代码,如下所示:

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

看似没有任何问题了,其实不然,主要问题在于instance = new SingleInstance()这行代码并不是原子性的,也就是说,这行代码需要处理器分为多步才能完成,其中主要包含两个操作,分配内存空间,引用变量指向内存,由于编译器可能会产生指令重排序的优化操作,所以两个步骤不能确定实际的先后顺序,假如线程A已经指向了内存,但是并没有分配空间,线程A阻塞,那么当线程B执行时,会发现Instance已经非空了,那么这时返回的Instance变量实际上还没有分配内存,显然是错误的。

4 单例模式中的volatile

volatile是Java提供的关键字,它具有可见性和有序性,被volatile修饰的写变量不能和之前的读写代码调整,读变量不能和之后的读写代码调整,所以,只要把instance加上volatile关键字就可以避免3中的问题了。代码如下所示:

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

猜你喜欢

转载自blog.csdn.net/xdzhouxin/article/details/81192344