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

单例模式是为了确保类只能生成一个对象,通常是该类需要消耗较多的资源或在逻辑上没有多个实例的情况。一般需要将构造函数私有化,使得用户无法手动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;
}
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值