设计模式之单例模式以及各种单例模式的优劣(JAVA讲解)

单例模式在设计模式中可以说是比较重要的一种模式了,理解起来很简单:在一个系统中,一个类只有一个实例对象。

但是具体实现起来,就会有各种需要考虑的问题在里面。接下来我将把我最近学习的感悟分享出来,有不对的地方还希望大家指正!谢谢了。

1、经典饿汉式:

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

特点:类加载(初始化)的时候就创建了对象,构造函数私有化保证了单例,

缺点:如果这个类特别庞大,初始化时将会特别缓慢,还有就是如果我们用不到这个类,它仍然会创建出来,浪费了资源。

2、经典懒汉式:

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

特点:通过构造函数私有化以及getInstance()方法加上synchronized关键字修饰来保证单例,并且只会在我们第一次需要这个对象的时候创建对象。

缺点:synchronized关键字是一个重锁(对象锁),它会每次调用getInstance(),都要对对象上锁,事实上,只有在第一次创建对象的时候需要加锁,之后就不需要了。

3、懒汉式变种—双重检查结构(不加volatile关键字修饰):

package cn.hzy.creationPattern.singleton;

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

特点:属于懒汉式的变种,上面懒汉式的特点都有,但是这里优化了性能问题,没有给getInstance()方法加锁,而是只给instance = new Singleton3();加锁,也就是说只在初始化的时候会加锁,后面的访问因为instance!=null,就不会加锁。

缺点:乍一看这种模式既没有线程安全问题,又保证了单例,貌似完美了,但是JVM在创建对象的时候有可能为了优化性能而进行指令重排,

看似简单的一句     instance = new Singleton3();   JVM在创建对象的时候会有三个步骤:

1、给Singleton3分配一个内存空间

2、初始化Singleton3(也就是创建Singleton3对象)

3、将instance指向刚分配的内存空间地址

但是有可能JVM为了编译的优化提高效率就有可能变成下面一种顺序:

1、给Singleton3分配一个内存空间

2、将instance指向刚分配的内存空间地址

3、初始化Singleton3(也就是创建Singleton3对象)

其实这种情况在单线程情况下是毫无影响的,结果都一样,但是如果在多线程情况下,就有可能导致错误。

比如:A、B两个线程访问getInstance()方法,A先进入第一个if判断,然后进入synchronized块,开始初始化Singleton3,由于发生了指令重排,将instance指向刚分配的内存空间地址(此时未创建Singleton3对象),在这个时候,B访问getInstance()方法,B进入第一个if判断,因为instance已经指向了一个存在的内存空间地址,即instance!=null,此时直接返回instance(未初始化),然后再调用的时候如果A还没有初始化完毕那么就会报空指针错误。(概率很低)

解决方案:加上volatile关键字修饰,

volatile:

特性一:内存可见性,即线程A对volatile变量的修改,其他线程获取的volatile变量都是最新的。

特性二:可以禁止指令重排序。

修改如下:将   private static Singleton3 instance = null;   改为   private static volatile Singleton3 instance = null;

4、静态内部类:

package cn.hzy.creationPattern.singleton;

public class Singleton4 {
    private Singleton4() {}
    
    public static Singleton4 getInstance() {
        return SingletonFactory.instance;
    }
        
    private static class SingletonFactory {
	private static Singleton4 instance = new Singleton4();
    }
}

特点:按特征也是属于懒汉模式,因为只会在我们需要用的时候才会创建实例对象,这里通过构造函数私有化,使用内部类来维护单例的实现,因为JVM内部的机制能够保证当一个类被加载的时候,这个类的加载过程是线程互斥的。这样当我们第一次调用getInstance的时候,JVM能够帮我们保证instance只被创建一次, 并且会保证把赋值给instance的内存初始化完毕,这样我们就不用担心Singleton3出现的问题。同时该方法也只会在第一次调用的时候使用互斥机制,这样就解决了低性能问题。

缺点:貌似这个就完美了,但是静态内部类也有着一个致命的缺点,就是传参的问题,由于是静态内部类的形式去创建单例的,故外部无法传递参数进去的。

5、枚举:

public enum Singleton {
    INSTANCE;
    public void method() {
    }
}

直接调用SingleTon.INSTANCE就是单例。

特点:创建枚举默认就是线程安全的

优点:简直不要太多,1、写法简单,对比上面的实例就能发现。2、可以防止反射攻击。

针对上面的反射攻击我这里简单说一下:在上面的1、2、3、4种单例模式里面,如果不对构造函数做一些安全处理,我们可以很轻松通过反射拿到构造器并且创建不只一个实例对象,就不再是单例了。但是对于枚举,即时你通过反射拿到构造器,在创建对象实例的时候也会报错,因为枚举是可以防止反射攻击的。

怎么对构造函数做一些安全处理?

可以立一个flag,在创建一个对象实例后,改变flag的值,通过判断抛出异常。

比如:

private static boolean flag = false;
private Singleton (){
    synchronized (Singleton .class) {
        if(false == flag){
            flag = !flag;
        } else {
            throw new RuntimeException("单例模式正在被反射攻击!!!");
        }  
    }
}

通过在构造函数里面增加一个判断来保证不被反射攻击。


总结:通过上面的分析我们能够看到各种单例模式优劣,其实实际开发中第一种反而用得比较多,因为既然我们都设置了这么一个单例类,那么肯定是系统中会需要这个类,(所以至于创建了这个单例类而又不去用它这种情况,肯定是当时得设计人员酒后设计。。。),至于初始化比较慢,慢就慢呗,那一点影响不在乎,而且在用的时候快啊!!!

还有就是我更加推荐使用枚举,优点上面已经说了。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值