设计模式 - 单例模式

知识点:

1.饿汉单例模式

2.饿汉单例模式防反射

3.饿汉单例模式防序列化

4.饱汉单例模式

5.饱汉单例模式+同步支持多线程

6.double checked locking 单例模式

7.静态内部类实现单例模式

8.枚举实现单例模式


1.饿汉单例模式

如下面代码所示,下面单例类提供一个私有的构造函数,静态的final的成员变量,保证该类只在类加载器加载的时候实例化一次,保证唯一实例,访问则通过公有的静态方法访问。看似很完美,实际上有三个问题:

1.没有实现懒加载,如果我们不会用到这个类,这个类还是会实例化一次,如果是很复杂的对象,会影响性能。

2.我们可以通过反射调用私有构造器生成第二个实例。

3.如果该实例需要序列化到文件系统,在从硬盘读取出来,会生成第二个实例。

public static class Singleton{
    private static final Singleton instance = new Singleton();
    private Singleton(){
        //do something
    }
    public static Singleton getInstance(){
        return instance;
    }
}

2.饿汉单例模式防反射

看似完美的饿汉单例模式,居然有三个问题,是不是没想到。不要急,我们来一个个的解决,先解决反射生成多个实例的问题。看下面代码,反射是通过访问类的私有构造函数来构造第二个实例,加斜体代码,如果实例已经存在,我们就报错发火。解决了反射的问题。还剩下懒加载和序列化的问题。

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

3.饿汉单例模式防序列化

如果这个单例对象有可能会保存到文件中,重新读取出来就会生成另一个实例,大家可以写个简单的程序验证(先写对象到文件中,然后读取出来,两个对象做“==”判断)。为什么会出现这种情况了?(我这里简单的介绍一下,后面的文章我会详细介绍Java序列化的知识奋斗),其实很简单,反序列化的时候其实是通过反射调用类的无参构造方法重新生成的对象。那么上面的抛出异常就不合适了,反系列化的时候会有问题。解决方式看下面代码,加上斜体部分代码后,反序列化的过程中,返回该方法中的对象而不会调用构造方法构造新的对象,而会返回已经构造好的单例对象(具体原理请关注我的后续文章)。好的序列化的问题也解决了,只剩下懒加载的问题了。

public static class Singleton{
    private static final Singleton instance = new Singleton();
    private Singleton(){
    }
    public static Singleton getInstance(){
        return instance;
    }
    private Object readResolve() {
        return singleton;
    }
}

4.饱汉单例模式

如下面代码所示,公有的静态构造函数,在调用的时候,如果对象为空,才调用私有的构造函数创建对象。解决了饿汉单例模式懒加载的问题。反射和序列化的问题可以用同样的方式去解决。看似很完美,可还是有个问题:

1.多线程下有可能会出现多个实例,比如两个线程运行到“if (instance == null)”,都会认为没有实例,然后一起创建该对象。

public class SingleTon {
    private static SingleTon instance;

    private SingleTon() {
    }

    public static SingleTon getInstance() {
        if (instance == null) {
            instance = new SingleTon();
        }
        return instance;
    }
}

5.饱汉单例模式+同步支持多线程

怎么实现同步了,使用大家熟悉的synchronized关键字同步就行了,看下面代码,多线程调用getInstance方法的时候,只有一个线程实例化对象,其他的线程都在等待。解决了多线程的问题,但还是有个问题偷笑

1.效率问题,单例对象创建后,多线程读取的时候,这个同步操作完全是多余的,读的时候同步完成没有意义,只会影响运行速度。

public class SingleTon {
    private static SingleTon instance;

    private SingleTon() {
    }

    public static synchronized SingleTon getInstance() {
        if (instance == null) {
            instance = new SingleTon();
        }
        return instance;
    }
}
6.double checked locking 单例模式

终于到了大家最熟悉的双重检测单例模式,看下面代码,调用getInstance的时候,如果对象是空的,我们才同步创建对象,保证单例,读取的时候,如果对象存在,直接返回,解决了上面的效率问题。真的完美了么?其实还是有个问题:

1."instance = new SingleTon()" 这句语句不是原子的,分为三步,a.分配内存 b.构造 c.赋值. JVM会自己对指令重排(对指令重排不了解的,我后面的文章会详细讲解),C有可能会和B交换位置。如果对象还没有构造,但是引用已经有了,判断的时候,另外的线程会以为对象已经存在(引用不为null),但对象并没有真正构造完成。解决这个问题的方式也很简单,在第二句加上volatile关键字. volatile(后面的文章会详细介绍volatile关键字)的作用之一可以防止指令重排。到这里,所有的问题都解决了,但大家会问,虽然没什么问题,但是觉得写法太复杂,下面再介绍两种简单的写法。

public class Singleton {   

    private static Singleton instance = null;   

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

7.静态内部类实现单例模式

看下面代码,把实例化放在静态内部类里面,真正调用的时候才实例化,简单的实现了懒加载(调用的时候实例化)和线程同步(静态类内部类只会被加载一次)。完成了和6同样的功能,但代码更简单。

public class Singleton
{
	private Singleton(){ }
	
	public static Singleton getInstance()
	{
		return Nested.instance;		
	}
	
	//在第一次被引用时被加载
	static class Nested
	{
		private static Singleton instance = new Singleton();
	}
	
}

8.枚举实现单例模式

最后一种是Effective Java推荐的方式,看下面代码,INSTANCE就是单例的枚举对象,使用方式"Singleton.INSTANCE.doSomeThing()"。他是线程安全的,类加载的时候只会创建一次,他是防反射的,应为枚举类实际上是一个抽象类,是不能够通过反射实例化的,他是支持序列化的并不会产生多个对象(枚举本身特性),唯一的缺点是没有懒加载。(关于枚举为什么这么吊的原理,后面的文章会详细解释)

public enum Singleton {

    INSTANCE;

    public void doSomeThing(){

    }

}
题外话:简单的单例模式并不简单,是设计模式中最复杂的,单例模式你真正弄明白了么?这篇文章中还有一些原理的东西,像指令重排,枚举等等,请关注我的后续文章。


评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值