设计模式之单例模式

  单例模式说简单,也很简单,说复杂,也可以让你想不到。从接触到现在,做一个总结,方便以后复习。

  实现单例模式的思路是:

  • 一个类能返回对象一个引用(永远是同一个)和一个获得该实例的方法(必须是静态方法,通常使用getInstance这个名称);
  • 当我们调用这个方法时,如果类持有的引用不为空就返回这个引用,如果类保持的引用为空就创建该类的实例并将实例的引用赋予该类保持的引用;
  • 同时我们还将该类的构造函数定义为私有方法,这样其他处的代码就无法通过调用该类的构造函数来实例化该类的对象,只有通过该类提供的静态方法来得到该类的唯一实例。
    ——来自维基百科

根据上面的要求和描述,就有以下实现了。


第一版

public class Singleton{
	// 单例模式
	private static Singleton instance = null; // 单例对象 , 懒汉式
	private Singleton(){ } // 私有构造函数	

	// 静态工厂方法
	public static Singleton getInstance(){
		if(instance == null){
			instance = new Singleton();
		}
		return instance;
	}

}

  • 要想让一个类只能构建一个对象,自然不能让它随便去做new操作,因此Signleton的构造方法是私有的。

  • instance是Singleton类的静态成员,也是我们的单例对象。它的初始值可以写成Null,也可以写成new Singleton()。

    • 如果单例初始值是null,还未构建,则构建单例对象并返回。这个写法属于单例模式当中的懒汉模式。
    • 如果单例对象一开始就被new Singleton()主动构建,则不再需要判空操作,这种写法属于饿汉模式。
  • getInstance是获取单例对象的方法。

第一版不是线程安全的

假设Singleton类刚刚被初始化,instance对象还是空,这时候两个线程同时访问getInstance方法:

image_1cj7m96gk1nft1pbg1ecv1geuun247.png-85.7kB
因为Instance是空,所以两个线程同时通过了条件判断,开始执行new操作:

image_1cj7m9f4bsjo531ts7134rlp14k.png-79kB


第二版

这样一来,显然instance被构建了两次。让我们对代码做一下修改:

 //单例模式

    private static  Singleton instance = null; // 单例对象

    private Singleton(){} // 私有构造函数

    //静态工厂方法
    public static  Singleton getInstance(){

        if (instance == null) {  //双重检测机制
            synchronized (Singleton.class) { //同步锁
                if (instance == null) { //双重检测机制  // 像这样两次判空的机制叫做双重检测机制。
                    instance = new Singleton();
                }
            }
        }
        return  instance;
    }
    
  • 为了防止new Singleton被执行多次,因此在new操作之前加上Synchronized
    同步锁,锁住整个类(注意,这里不能使用对象锁)。

  • 进入Synchronized 临界区以后,还要再做一次判空。因为当两个线程同时访问的时候,线程A构建完对象,线程B也已经通过了最初的判空验证,不做第二次判空的话,线程B还是会再次构建instance对象。

我们再来回顾上面的情景
image_1cj7m6oer1d4qdhfkv11pg316b1m.png-106.9kB

image_1cj7m775hs0ofcaj13mfo13c12j.png-115.5kB

image_1cj7m7gae14k4odm1q421l9a16jg30.png-106.5kB

image_1cj7m7tammfkrmtadl12261fcc3d.png-107.4kB

image_1cj7m85srpfg1i3raaj1amh1ncp3q.png-102.9kB

的确解决了第一版的多线程下的问题。但是呢,如果你了解过指令重排序呢,还会这样认为?

第二版还是有问题,就是指令重排序

假设这样的场景,当两个线程一先一后访问getInstance方法的时候,当A线程正在构建对象,B线程刚刚进入方法:

image_1cj7mg82qlj51j1hemh1qj8vqb5h.png-109.5kB

这种情况表面看似没什么问题,要么Instance还没被线程A构建,线程B执行 if(instance == null)的时候得到true;要么Instance已经被线程A构建完成,线程B执行 if(instance == null)的时候得到false。

真的如此吗?答案是否定的。这里涉及到了JVM编译器的指令重排。

指令重排是什么意思呢?比如java中简单的一句 instance = new Singleton,会被编译器编译成如下JVM指令:

memory =allocate();    //1:分配对象的内存空间 
ctorInstance(memory);  //2:初始化对象 
instance =memory;     //3:设置instance指向刚分配的内存地址 

但是这些指令顺序并非一成不变,有可能会经过JVM和CPU的优化,指令重排成下面的顺序:

memory =allocate();    //1:分配对象的内存空间 
instance =memory;     //3:设置instance指向刚分配的内存地址 
ctorInstance(memory);  //2:初始化对象 

当线程A执行完1,3,时,instance对象还未完成初始化,但已经不再指向null。此时如果线程B抢占到CPU资源,执行 if(instance == null)的结果会是false,从而返回一个没有初始化完成的instance对象。如下图所示:
image_1cj7mhfb3g5313e315001fo0eln5u.png-167.2kB

image_1cj7mhpc913q3gl51q2g1h5lgtf6b.png-167.7kB

如何避免这一情况呢?我们需要在instance对象前面增加一个修饰符volatile。


第三版

 //单例模式

    private static  volatile Singleton instance = null; // 单例对象

    private Singleton(){} // 私有构造函数

    //静态工厂方法
    public static  Singleton getInstance(){
        if (instance == null) {  //双重检测机制
            synchronized (Singleton.class) { //同步锁
                if (instance == null) { //双重检测机制
                    instance = new Singleton();
                }
            }
        }
        return  instance;
    }

经过volatile的修饰,当线程A执行instance = new Singleton()的时候,JVM执行顺序是什么样?始终保证是下面的顺序:

memory =allocate();    //1:分配对象的内存空间 
ctorInstance(memory);  //2:初始化对象 
instance =memory;     //3:设置instance指向刚分配的内存地址 

如此在线程B看来,instance对象的引用要么指向null,要么指向一个初始化完毕的Instance,而不会出现某个中间态,保证了安全。

volatile关键字不但可以防止指令重排,也可以保证线程访问的变量值是主内存中的最新值。

volatile关键字


序列化与反序列化

  然而这样就真能保证单例吗?答案就是不能了,因为如果过该类implements Serializable,那么就会在反序列化的过程中再创一个对象。这就不满足一个类只有一个单例实例,从而破坏了单例模式。

这个问题的解决办法就是在反序列化时,指定反序化的对象实例。添加如下方法:

 private Object readResolve() {
        return getInstance();
    }

这下总行了吧? no,还有 反射机制,通过反射可以重复构造对象,从而破坏了单例模式。

用反射破坏单例模式示例


     public static void main(String[] args) throws Exception {
        //利用反射打破单例

        //获得构造器
        Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
        //设置为可访问
        constructor.setAccessible(true);

        //构造两个不同的对象
        Singleton singleton1 = constructor.newInstance();
        Singleton singleton2 = constructor.newInstance();

        //验证是否是不同对象
        System.out.println(singleton1.equals(singleton2)); 

    }

如何防止通过反射来重复构造对象??用枚举实现单例模式

  JVM会阻止反射获取枚举类的私有构造方法,使用枚举类实现单例模式不仅能够防止反射构造对象,而且可以保证线程安全。有一个唯一缺点就是,它并非使用懒加载其单例对象实在枚举类被加载的时候进行初始化的。

  使用枚举实现的单例模式,不但可以防止利用反射强行构建单例对象,而且可以在枚举类对象被反序列化的时候,保证反序列的返回结果是同一对象。

public enum Singleton3 {

    INSTANCE;
}

补充: 用静态内部类实现单例模式


public class Singleton {

    private static class SingletonFactory{
        private  static final Singleton instance = new Singleton();
    }

    private Singleton(){}

    public static Singleton getInstance(){
        return SingletonFactory.instance;
    }
}

这里有需要说明几个点:

  1. 从外部无法访问静态内部类SingletonFactory,只有当调用SingletonFactory.getInstance方法的时候,才能得到单例对象instance

  2. instance对象初始化的时机并不是在单例类Singleton被加载的时候,而是在调用getInstance方法,使得静态内部类SingletonFactory被加载的时候。因此这种实现方式是利用classloader的加载机制来实现懒加载,并保证构建单例的线程安全。

静态内部类的实现方式,还是前面的单例模式实现都一样,无法防止利用反射来重复构造对象。

单例模式实现是否线程安全是否支持懒加载是否防止反射构建
双重锁检测
静态内部类
枚举

这里贴两个关于序列化与反序列化的文章,写的很详细

为什么在Java中,实现了java.io.Serializable接口就能启用其序列化功能

java.io.Serializable接口以外的其他序列化接口

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值