单例模式详解

单例模式
单例模式是最简单的设计模式之一,它属于创建型设计模式,提供了一种创建对象的方法。这种模式有一个单例类,它可以创建自己的对象,并且确保只有一个对象被创建(也就是构造方法私有)。并且这个类提供了一种访问其唯一对象的方式,可以直接访问,不需要实例化该类的对象。

总结就是三点:

  1. 单例类只能有一个实例。
  2. 单例类必须自己创建自己的唯一实例
  3. 单例类必须给所有其他对象提供这一实例

使用场景

当想要控制实例数目(表示全局唯一类),节省系统资源,处理资源访问冲突的时候可以使用单例模式,它主要解决一个全局使用的类频繁的创建和销毁,是通过系统是否已经有这个单例,如果有就返回,没有就创建这样的方法来解决的。

缺点:没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。细分下来有以下几点:

  1. 单例对 OOP 特性的支持不友好,对其中的抽象、继承、多态都支持得不好
  2. 单例会隐藏类之间的依赖关系
  3. 单例对代码的扩展性不友好,当需要在代码中创建两个实例或多个实例,那就要对代码有比较大的改动
  4. 单例对代码的可测试性不友好
  5. 单例不支持有参数的构造函数

单例模式的实现
单例类的实现有线程安不安全的两种懒汉式,饿汉式,双检锁,匿名内部类,枚举这6种:

1、线程不安全的懒汉式:

public class Singleton {  
    private static Singleton instance;  //其属性就包括自己这个类
    private Singleton (){}  //构造方法私有
  
    public static Singleton getInstance() {//提供直接访问其唯一对象的方式
    if (instance == null) {  
        instance = new Singleton();  //第一次访问才创建自己的对象
    }  
    return instance;  
    }  
}

这种方式支持懒加载(第一次调用才初始化,避免内存浪费),实现也很容易。但最大的问题是线程不安全,也就是不支持多线程,因为是因为访问方法没有加锁 synchronized。

举个例子:我们假设有多个线程1,线程2都需要使用这个单例对象。而恰巧,线程1在判断完s==null后突然交换了cpu的使用权,变为线程2执行,由于s仍然为null,那么线程2中就会创建这个Singleton的单例对象。之后线程1拿回cpu的使用权,而正好线程1之前暂停的位置就是判断s是否为null之后,创建对象之前。这样线程1又会创建一个新的Singleton对象。

2、线程安全的懒汉式:

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

它与线程不安全的懒汉式唯一的区别就是访问方法加了synchronized,这样就能在多线程情况下工作了,但加锁了就会导致其效率很低。

3、饿汉式:

public class Singleton {  
    private static Singleton instance = new Singleton(); //没访问之前就已经初始化了
    private Singleton (){}  
    public static Singleton getInstance() {  
    return instance;  
    }  
}

饿汉式,没访问之前就已经初始化单例了,容易产生垃圾对象,这也是它的缺点所在。
优点是没有加锁和早创建单例使得其执行效率提高。同时因为instance在线程访问单例对象之前就已经创建好了。再加上,由于一个类在整个生命周期中只会被加载一次,因此该单例类只会创建一个实例。就是说,线程每次都只能也必定只可以拿到这个唯一的对象。因此饿汉式单例天生就是线程安全的。

4,双检锁(DCL,即 double-checked locking)

public class Singleton {  
    private volatile static Singleton singleton;  //(给singleton对象加了volatile 关键字)
    private Singleton (){}  
    public static Singleton getSingleton() {  
    if (singleton == null) {  
    	//可能多个线程同时进入这里
        synchronized (Singleton.class) {  //(给整个类加了锁,以下的为同步代码块)线程1到这里继续往下执行,其他线程等待
        if (singleton == null) { //线程1到这里发现singleton 为空,继续执行if代码块
            singleton = new Singleton();  
             /**
              * 由于编译器的优化、JVM的优化、操作系统处理器的优化,可能会导致指令重排(happen-before规则下的指令重排,执行结果不变,指令顺序优化排列)
              * new Singleton3()这条语句大致会有这三个步骤:
              * 1.在堆中开辟对象所需空间,分配内存地址
              * 2.根据类加载的初始化顺序进行初始化
              * 3.将内存地址返回给栈中的引用变量(instance变量被分配了地址,就不为null了)
              * 但是由于指令重排的出现,这三条指令执行顺序会被打乱,可能导致3的顺序和2调换
              */
        }  
        }  
    }  
    return singleton;  
    }  
}

双检锁可以说是懒汉式的升级版,支持懒加载,它既是线程安全的,同时又能保证高性能。

为什么singleton对象前要加volatile 关键字?
如果没加volatile 关键字的话,在多线程情况下,由于指令重排导致3,2的顺序调换,会导致以下问题的出现:首先第一个线程执行到了3号指令(instance变量被分配了地址,不为null了),但对象未初始化。此时!第一个if语句进行判断时结果为true,自然而然在使用instance时会出错。解决的方法便是在instance变量上加上volatile关键字,加上volatile关键字后会禁止该变量的指令重排,从而达到线程安全

为什么要有两次if判断?
内层判断:当多个线程同时进入到第一层if代码块内时,线程1到第七行(同步代码块)继续往下执行,其他线程等待,线程1到这里发现singleton 为空,继续执行if代码块实例化Singleton对象,执行完成后退出同步代码块,然后线程2进入第七行内,如果在这里不加内层判断,就会造成instance再次实例化,这就违背了单例模式的单例二字。由于这里加了判断,线程2在这里发现singleton 已经被实例化了就跳出if代码块了。

外层判断:在多个线程依次调用了getInstace函数时,当线程1走完了内层判断,对象实例化了,这个时候线程3也调用了getInstace函数,如果没有加外层的判断,线程3还是要继续等待线程2的完成,而加上外层判断,就不需要等待了,直接返回了实例化的对象。

我的理解:外层的判断是为了提高效率,里层的判断就是第一次实例化需要。

那么学到这里就有一个问题了!支持懒加载的的双检锁是不是就要比饿汉式更优呢?

可能有人觉得饿汉式不好,因为不支持延迟加载,如果实例占用资源多或初始化耗时长,提前初始化实例是一种浪费资源的行为。最好的方法应该在用到的时候再去初始化。不过,我并不认同这样的观点。
如果初始化耗时长,那我们最好不要等到真正要用它的时候,才去执行这个耗时长的初始化过程,这会影响到系统的性能(比如,在响应客户端接口请求的时候,做这个初始化操作,会导致此请求的响应时间变长,甚至超时)。采用饿汉式实现方式,将耗时的初始化操作,提前到程序启动的时候完成,这样就能避免在程序运行的时候,再去初始化导致的性能问题。
如果实例占用资源多,按照 fail-fast 的设计原则(有问题及早暴露),那我们也希望在程序启动时就将这个实例初始化好。如果资源不够,就会在程序启动的时候触发报错,我们可以立即去修复。这样也能避免在程序运行一段时间后,突然因为初始化这个实例占用资源过多,导致系统崩溃,影响系统的可用性。
所以说:支持懒加载的双重检测不比饿汉式更优

5,静态内部类

public class Singleton {  
    private static class SingletonHolder {  //静态内部类实例化对象
    	private static final Singleton INSTANCE = new Singleton();  
    }  
    private Singleton (){}  
    public static final Singleton getInstance() {  如果没有到这里,那么不会加载上面的内部类
    	return SingletonHolder.INSTANCE;  
    }  
}

静态内部类线程安全,支持懒加载(能达到双检锁方式一样的功效,但实现更简单):外部类加载时并不需要立即加载内部类,内部类不被加载则不去初始化INSTANCE,故而不占内存。具体来说当SingleTon第一次被加载时,并不需要去加载SingleTonHoler,只有当getInstance()方法第一次被调用时,使用INSTANCE的时候,才会导致虚拟机加载SingleTonHoler类。这种方法不仅能确保线程安全,也能保证单例的唯一性,同时也延迟了单例的实例化。

那么具体是如何实现线程安全的?
首先类加载过程中的最后一个阶段:即类的初始化,它的本质就是执行类构造器的clinit方法。JVM内部会保证一个类的clinit方法在多线程环境下被正确的加锁同步,也就是说如果多个线程同时去进行“类的初始化”,那么只有一个线程会去执行类的clinit方法,其他的线程都要阻塞等待,直到这个线程执行完clinit方法。然后执行完clinit方法后,其他线程唤醒,但是不会再进入clinit方法。也就是说同一个加载器下,一个类型只会初始化一次。
回到这个代码中,这里的静态变量的赋值操作进行编译之后实际上就是一个clinit代码,当我们执行getInstance方法的时候,会导致SingleTonHolder类的加载,类加载的最后会执行类的初始化,但是即使在多线程情况下,这个类的初始化的代码也只会被执行一次,所以他只会有一个实例。

6、枚举

public enum Singleton {  
    INSTANCE;  
    public void doSomething() {
        System.out.println("doSomething");
    }
}

这种实现方式还没有被广泛采用,在effective java这本书中说道,最佳的单例实现模式就是枚举模式,它更简洁,利用枚举的特性,让JVM来帮我们保证线程安全和单一实例的问题,自动支持序列化机制。
不过,由于 JDK1.5 之后才加入 enum 特性,用这种方式写不免让人感觉生疏,在实际工作中,也很少用。

总结:一般情况下,不建议使用第 1 种和第 2 种懒汉方式,建议使用第 3 种饿汉方式。只有在要明确实现 lazy loading 效果时,才会使用第 5 种静态内部类。如果涉及到反序列化创建对象时,可以尝试使用第 6 种枚举方式。如果有其他特殊的需求,可以考虑使用第 4 种双检锁方式。

单例有什么替代解决方案?
为了保证全局唯一,除了使用单例,我们还可以用静态方法来实现。不过,静态方法这种实现思路,并不能解决我们之前提到的问题。如果要完全解决这些问题,我们可能要从根上,寻找其他方式来实现全局唯一类了。比如,通过工厂模式、IOC 容器(比如 Spring IOC 容器)来保证,通过程序员己来保证(自己在编写代码的时候自己保证不要创建两个类对象)。
模式没有对错,关键看你怎么用。如果单例类并没有后续扩展的需求,并且不依赖外部系统,那设计成单例类就没有太大问题。对于一些全局的类,我们在其他地方 new 的话,还要在类之间传来传去,不如直接做成单例类,使用起来简洁方便

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值