欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页

单例模式(Singleton)的几种实现

程序员文章站 2022-03-07 18:06:48
...

单例模式是为了确保类只能生成一个对象,通常是该类需要消耗较多的资源或在逻辑上没有多个实例的情况。一般需要将构造函数私有化,使得用户无法手动new出对象,还需要向外暴露一个公有的静态方法以便获取单例对象。

本篇文章主要总结单例模式在Java语言中的实现方法。

1、 饿汉模式

顾名思义,饿汉嘛,经不起等待,也就是在使用之前就已经初始化好了单例对象。

public class Singleton {

	//静态成员变量在类加载时就已经初始化。
    private static Singleton sInstance = new Singleton();

    private Singleton(){}

    public static Singleton getInstance(){
        return sInstance;
    }
}

这样效率比较低,这个实例很有可能在之后程序运行时并未被使用,就浪费了系统资源。

2、 懒汉模式

懒汉就会把事情推迟到最后一刻,也就是在外部调用getIntsance静态方法获取单例对象时再初始化。
相应的,在getIntsance方法中需要进行判断,若之前单例对象已经new出来了就直接返回即可。

public class Singleton {

    private static Singleton sInstance;

    private Singleton(){}

    public static Singleton getInstance(){
        if(sInstance == null){
            sInstance = new Singleton();
        }
        return sInstance;
    }
}

这种实现线程不安全。若有多个进程同时进入了if判断,每个线程都会new一个对象,就不能叫做单例了。

可以通过在方法上加锁来解决。

public class Singleton {

    private static Singleton sInstance;

    private Singleton(){}

    public synchronized static Singleton getInstance(){
        if(sInstance == null){
            sInstance = new Singleton();
        }
        return sInstance;
    }
}

但是加了锁之后效率就降低了,只能有一个线程进入到getInstance方法中。我们要想清楚真正应该锁住的是“判断+new”的操作,因为一旦对象new出来就可以直接返回了,也不会存在冲突情况。

我们可以在上一版实现的基础上再次改进,只让synchronized 锁住一部分代码块来提高效率。

public class Singleton {

    private static Singleton sInstance ;

    private Singleton(){}

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

那这个版本是不是就完美了呢?当然不是!

3、DCL(Double Check Lock)

还是我们在上面提到的——“一旦对象new出来就可以直接返回了,也不会存在冲突情况。”,在这种情况下线程根本没必要进入到锁住的代码块,所以我们还需要再加一个外层的判断。

public class Singleton {

    private static Singleton sInstance;

    private Singleton(){}

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

其实这一种实现就是DCL了,第一层的check是为了避免不必要的同步,第二层的check是为了在null的情况下创建实例。

但这一版本还是有点瑕疵,因为指令重排的存在。

简单来说,我们写的JAVA代码在底层都是一条条的指令,可以实现一些操作,比如说赋值等。JVM为了提高程序执行的效率,会按照一定的规则允许进行指令优化,可能不会按照我们写代码时的顺序严格执行。

sInstance = new Singleton();

这行代码最终编译成的汇编指令主要做了三件事:

  1. 给Singleton的实例分配内存;
  2. 调用Singleton的构造函数进行成员的初始化;
  3. 将sInstance 对象指向分配的内存空间。(sInstance 此时不为null)

由于指令重排的存在,执行顺序可能并非我们预想的1-2-3,有可能是1-3-2。在后者这种情况下,执行完3之后尚未执行2时,线程进行了切换,在其他线程中sInstance 就非空了,但实际上成员并未进行初始化,在之后的使用中可能会出问题,这就是DCL失效问题。

为了避免这种情况发生,我们在声明实例时加上一个volatile关键字可以禁止指令重排。

private volatile static Singleton sInstance;

4、静态内部类

DCL的写法稍有不慎就出错。静态内部类实现单例就更为简便。

public class Singleton {
    private Singleton(){}

    public static Singleton getInstance(){
        return InnerInstance.sInstance;
    }

    private static class InnerInstance{
    	private static final Singleton sInstance = new Singleton();
    }
}

只有在第一次调用Singleton 的getInstance方法时才会导致sInstance初始化。这种方法是被推荐使用的。

5、枚举

枚举在JAVA中和普通的类是一样的,可以拥有字段和方法。枚举市里的创建默认是线程安全的,在任何情况下都是一个单例。

public enum Singleton {
    INSTANCE;

    public void myMethod(){
        System.out.println("Hello");
    }
}

以上五种是比较常见的单例实现方式,但前四种在一个特殊情况下会重新创建新对象的情况——反序列化。
即时我们已经将构造函数私有化了,反序列化时依然可以通过特殊途径去创建一个新的实例,相当于调用了该类的构造函数。

为了杜绝在反序列化时重新创建对象,需要加入以下方法:

private Object readResolve() throws ObjectStreamException{
        return sInstance;
}
相关标签: 设计模式