设计模式之单例模式,看这篇文章就够了

522 阅读4分钟

单例模式是23种设计模式中最简单、最常见的设计模式之一,许多人学习设计模式,第一个接触到的就是单例模式。

单例模式属于创建型模式,其目的是在当前进程中只创建一个实例,也可能是一个线程中属于单例。

想要写出安全又简洁的单例模式,如果不注意细节,很容易有潜在的bug,本文总结了最常见的4种单例模式写法,并分析每种优缺点。

懒汉模式

懒汉模式,就是在用的时候才会被创建,体现的是一种延迟加载的思想。

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

这种写法lazy loading很明显,但是致命的是在多线程环境下不能正常工作,再来看看并发安全的懒汉模式。

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

这种写法能够在多线程中很好的工作,而且看起来它也具备很好的lazy loading,但是synchronized加锁之后,每次获取单例都需要上锁,所以效率很低,99%情况下不需要同步。再看下它的优化版:

public class Singleton {  
    private volatile static Singleton singleton;  
    private Singleton () {}  
    public static Singleton getSingleton() {  
        if (singleton == null) {  
            synchronized (Singleton.class) {  
                if (singleton == null) {  
                    singleton = new Singleton();  
                }
            }
        }
        return singleton;  
    }  
}  

这就是大名鼎鼎的双重检查锁方式,也有人质疑这种单例模式的可靠性,这里笔者不做过多讲解,有兴趣的伙伴可以去网上搜索:double-checked locking is broken。

需要注意的是,singleton实例变量必须加volatile关键字。加volatile,这里主要使用了它的有序性特性,可以禁止指令重排。有许多的博客作者说是利用了volatile的可见性,并不是的。

singleton = new Singleton();

是由三个步骤组成的:

  1. 为对象分配内存

  2. 实例化对象

  3. 将引用指向对应的内存地址

第2,3步可能发生指令重排列,第一个线程先将singleton指向一个未实例化对象的内存地址,然后再进行实例化对象。

若此时第二个线程进行第一个非空判断时,则为false,会直接返回还没有实例化对象的内存地址,从而可能产生空指针异常。

饿汉模式

饿汉模式就是在使用前,实力已经被创建了。其实现代码如下:

public class Singleton {  
    private static Singleton instance = new Singleton();  
    private Singleton (){}  
    public static Singleton getInstance() {  
        return instance;  
    }  
}  

这种方式基于classloder机制避免了多线程的同步问题,instance在类装载时就实例化,显然没有达到lazy loading的效果,也有变种写法。

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

静态内部类

这是我在代码中比较常用的一种写法。

public class Singleton {
    private static class SingletonHolder {
        private SingletonHolder() {}
        private static final Singleton INSTANCE = new Singleton();
        public static final Singleton getInstance() {
            return INSTANCE;
        }
    }
    public static Singleton getSingleton() {
        return SingletonHolder.getInstance();
    }
}

或者:

public class Singleton2 {
    private Singleton2() {}
    private static final class Singleton2Holder {
        public static final Singleton2 INSTANCE = new Singleton2();
    }
    public static Singleton2 getInstance() {
        return Singleton2Holder.INSTANCE;
    }
}

这种方式同样利用了classloder的机制来保证初始化instance时只有一个线程,

这种方式与饿汉模式不同的是,是Singleton类被装载了,instance不一定被初始化,有lazy loading效果。

枚举

枚举和静态代码块的特性相似,使用枚举时,构造方法会被自动调用,也可以利用这个特性实现单例模式,不过比较少见。

public class EnumSingleton{
    private EnumSingleton(){}
    public static EnumSingleton getInstance(){
        return Singleton.INSTANCE.getInstance();
    }
    
    private static enum Singleton{
        INSTANCE;
        
        private EnumSingleton singleton;
        //JVM会保证此方法绝对只调用一次
        private Singleton(){
            singleton = new EnumSingleton();
        }
        public EnumSingleton getInstance(){
            return singleton;
        }
    }
}

public static void main(String[] args) {
    EnumSingleton obj1 = EnumSingleton.getInstance();
    EnumSingleton obj2 = EnumSingleton.getInstance();
    System.out.println("obj1==obj2?" + (obj1==obj2));  // true
}

总结:

单例模式主要有懒汉模式、饿汉模式、内部类、基于枚举,4种写法,基于内部类的方式比较常见,也是比较推荐的方式。

设计模式单纯的使用比较简单,在许多复杂的业务场景,常常会把多种模式混合起来使用,比如单例模式+工厂模式。而且重要的是融会贯通,不在于死记硬背,这样才能灵活多变的应用。

END