漫话:如何给女友解释什么是单例模式?

周末了,临近五一劳动节,女友尚未想好要去哪里玩,还在看着各类攻略。我则在旁边一边看书默默的心疼着个人钱包。忽然女友开始发问:
java

什么是单例

单例模式,也叫单子模式,是一种经常使用的软件设计模式。在应用这个模式时,单例对象的类必须保证只有一个实例存在。算法

许多时候整个系统只须要拥有一个的全局对象,这样有利于咱们协调系统总体的行为。好比在某个服务器程序中,该服务器的配置信息存放在一个文件中,这些配置数据由一个单例对象统一读取,而后服务进程中的其余对象再经过这个单例对象获取这些配置信息。这种方式简化了在复杂环境下的配置管理。设计模式

举个简单的例子,就像中国的一夫一妻制度,夫妻之间只能是一对一的,也就是说,一个男子同时只能有一个老婆。这种状况就叫作单例。在中国,是经过《婚姻法》来限制一夫一妻制的。安全

男女双方来到民政局登记
if 男方目前已经有老婆{
    提醒二位没法结婚。并告知其当前老婆是谁。
}else{
    检查女方婚姻情况,其余基本信息核实。
    赞成双方结为夫妻。
}
复制代码

对于代码开发中,一个类同时只有一个实例对象的状况就叫作单例。那么,如何保证一个类只能有一个对象呢?bash

咱们知道,在面向对象的思想中,经过类的构造函数能够建立对象,只要内存足够,能够建立任意个对象。服务器

因此,要想限制某一个类只有一个单例对象,就须要在他的构造函数上下功夫。多线程

实现对象单例模式的思路是:并发

一、一个类能返回对象一个引用(永远是同一个)和一个得到该实例的方法(必须是静态方法,一般使用getInstance这个名称);函数

二、当咱们调用这个方法时,若是类持有的引用不为空就返回这个引用,若是类保持的引用为空就建立该类的实例并将实例的引用赋予该类保持的引用;性能

三、同时咱们还将该类的构造函数定义为私有方法,这样其余处的代码就没法经过调用该类的构造函数来实例化该类的对象,只有经过该类提供的静态方法来获得该类的惟一实例。

public class Singleton {  
    private static Singleton instance;  
    private Singleton (){}  

    public static Singleton getInstance() {  
    if (instance == null) {  
        instance = new Singleton();  
    }  
    return instance;  
    }  
}  
复制代码

以上Java代码,就实现了一个简单的单例模式。咱们经过将构造方法定义为私有,而后提供一个getInstance方法,该方法中来判断是否已经存在该类的实例,若是存在直接返回。若是不存在则建立一个再返回。

线程安全的单例

关于并发,能够参考《如何给女友解释什么是并行和并发》

在中国,想要拥有一个妻子,须要男女双方带着各自的户口本一块儿去民政局领证。民政局的工做人员会先在系统中查询双方的婚姻情况,而后再办理登记手续。之因此能够保证一夫一妻登记成功的前提是不会发生并发问题。

假设某男子能够作到在同一时间分别和两个不一样的女子来登记,就有一种几率是当工做人员查询的时候他并无结婚,而后就可能给他登记两次结婚。固然,这种状况在现实生活中是根本不可能发生的。

可是,在程序中,一旦有多线程场景,这种状况就很常见。就像上面的代码。

若是有两个线程同时执行到if(instance==null)这行代码,这是判断都会经过,而后各自会执行instance = new Singleton();并各自返回一个instance,这时候就产生了多个实例,就没有保证单例!

上面这种单例的实现方式咱们一般称之为懒汉模式,所谓懒汉,指的是只有在须要对象的时候才会生成(getInstance方法被调用的时候才会生成)。这有点像现实生活中有一种"生米煮成熟饭"的状况,到了必定要结婚的时候才开始去领证。

上面的这种懒汉模式并非线程安全的,因此并不建议在平常开发中使用。基于这种模式,咱们能够实现一个线程安全的单例的,以下:

public class Singleton {  
    private static Singleton instance;  
    private Singleton (){}  
    public static synchronized Singleton getInstance() {  
    if (instance == null) {  
        instance = new Singleton();  
    }  
    return instance;  
    }  
}  
复制代码

经过在getInstance方法上增长synchronized,经过锁来解决并发问题。这种实现方式就不会发生有多个对象被建立的问题了。

双重校验锁

上面这种线程安全的懒汉写法可以在多线程中很好的工做,可是,遗憾的是,这种作法效率很低,由于只有第一次初始化的时候才须要进行并发控制,大多数状况下是不须要同步的。

咱们其实能够把上述代码作一些优化的,由于懒汉模式中使用synchronized定义一个同步方法,咱们知道,synchronized还能够用来定义同步代码块,而同步代码块的粒度要比同步方法小一些,从而效率就会高一些。如如下代码:

public class Singleton {  
    private volatile static Singleton singleton;  
    private Singleton (){}  
    public static Singleton getSingleton() {  
    if (singleton == null) {  
        synchronized (Singleton.class) {  
        if (singleton == null) {  
            singleton = new Singleton();  
        }  
        }  
    }  
    return singleton;  
    }  
}  
复制代码

上面这种形式,只有在singleton == null的状况下再进行加锁建立对象,若是singleton!=null的话,就直接返回就好了,并无进行并发控制。大大的提高了效率。

从上面的代码中能够看到,其实整个过程当中进行了两次singleton == null的判断,因此这种方法被称之为"双重校验锁"。

还有值得注意的是,双重校验锁的实现方式中,静态成员变量singleton必须经过volatile来修饰,保证其初始化的原子性,不然可能被引用到一个未初始化完成的对象。

为何双重校验锁须要使用volatile来修饰静态成员变量singleton?为何线程安全的懒汉就不须要呢?关于这个问题,后续文章深刻讲解。

饿汉模式

前面提到的懒汉模式,实际上是一种lazy-loading思想的实践,这种实现有一个比较大的好处,就是只有真正用到的时候才建立,若是没被使用到,就一直不会被建立,这就避免了没必要要的开销。

可是这种作法,其实也有一个小缺点,就是第一次使用的时候,须要进行初始化操做,可能会有比较高的耗时。若是是已知某一个对象必定会使用到的话,其实能够采用一种饿汉的实现方式。

所谓饿汉,就是事先准备好,须要的时候直接给你就好了。这就是平常中比较常见的"先买票后上车",走正常的手续。

如如下代码,饿汉模式:

public class Singleton {  
    private static Singleton instance = new Singleton();  
    private Singleton (){}  
    public static Singleton getInstance() {  
    return instance;  
    }  
}   
复制代码

或者如下代码,饿汉变种:

public class Singleton {  
    private Singleton instance = null;  
    static {  
    instance = new Singleton();  
    }  
    private Singleton (){}  
    public static Singleton getInstance() {  
    return this.instance;  
    }  
}  
复制代码

以上两段代码其实没有本质的区别,都是经过static来实例化类对象。饿汉模式中的静态变量是随着类加载时被完成初始化的。饿汉变种中的静态代码块也会随着类的加载一块执行。

以上两个饿汉方法,其实都是经过定义静态的成员变量,以保证instance能够在类初始化的时候被实例化。

由于类的初始化是由ClassLoader完成的,这实际上是利用了ClassLoader的线程安全机制。ClassLoader的loadClass方法在加载类的时候使用了synchronized关键字。也正是由于这样, 除非被重写,这个方法默认在整个装载过程当中都是同步的(线程安全的)

除了以上两种饿汉方式,还有一种实现方式也是借助了calss的初始化来实现的,那就是经过静态内部类来实现的单例:

public class Singleton {  
    private static class SingletonHolder {  
    private static final Singleton INSTANCE = new Singleton();  
    }  
    private Singleton (){}  
    public static final Singleton getInstance() {  
    return SingletonHolder.INSTANCE;  
    }  
}  
复制代码

前面提到的饿汉模式,只要Singleton类被装载了,那么instance就会被实例化。

而这种方式是Singleton类被装载了,instance不必定被初始化。由于SingletonHolder类没有被主动使用,只有显示经过调用getInstance方法时,才会显示装载SingletonHolder类,从而实例化instance。

使用静态内部类,借助了classloader来实现了线程安全,这与饿汉模式有着殊途同归之妙,可是他有兼顾了懒汉模式的lazy-loading功能,相比较之下,有很大优点。

单例的破坏

前文介绍过,咱们实现的单例,把构造方法设置为私有方法来避免外部调用是很重要的一个前提。可是,私有的构造方法外部真的就彻底不能调用了么?

其实不是的,咱们是能够经过反射来调用类中的私有方法的,构造方法也不例外,因此,咱们能够经过反射来破坏单例。

除了这种状况,还有一种比较容易被忽视的状况,那就是其实对象的序列化和反序列化也会破坏单例。

如使用ObjectInputStream进行反序列化时,在ObjectInputStream的readObject生成对象的过程当中,其实会经过反射的方式调用无参构造方法新建一个对象。

因此,在对单例对象进行序列化以及反序列化的时候,必定要考虑到这种单例可能被破坏的状况。

能够经过在Singleton类中定义readResolve的方式,解决该问题:

/**
 * 使用双重校验锁方式实现单例
 */
public class Singleton implements Serializable{
    private volatile static Singleton singleton;
    private Singleton (){}
    public static Singleton getSingleton() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }

    private Object readResolve() {
        return singleton;
    }
} 
复制代码

枚举实现单例

在StakcOverflow中,有一个关于What is an efficient way to implement a singleton pattern in Java?的讨论:

如上图,得票率最高的回答是:使用枚举。

回答者引用了Joshua Bloch大神在《Effective Java》中明确表达过的观点:

使用枚举实现单例的方法虽然尚未普遍采用,可是单元素的枚举类型已经成为实现Singleton的最佳方法。

若是你真的深刻理解了单例的用法以及一些可能存在的坑的话,那么你也许也能获得相同的结论,那就是:使用枚举实现单例是一种很好的方法。

枚举实现单例:

public enum Singleton {  
    INSTANCE;  
    public void whateverMethod() {  
    }  
}  
复制代码

以上,就实现了一个很是简单的单例,从代码行数上看,他比以前介绍过的任何一种都要精简,而且,他仍是线程安全的。

这些,其实还不足以说服咱们这种方式最优。可是还有个相当重要的缘由,那就是:枚举可解决反序列化会破坏单例的问题

关于这个知识点,你们能够参考《为何我墙裂建议你们使用枚举来实现单例》这篇文章,里面详细的阐述了关于枚举与单例的全部知识点。

不使用synchronized实现单例

前面讲过的全部方式,只要是线程安全的,其实都直接或者间接用到了synchronized,那么,若是不能使用synchronized的话,怎么实现单例呢?

使用Lock?这固然能够了,可是其实根本仍是加锁,有没有不用锁的方式呢?

答案是有的,那就是CAS。CAS是一项乐观锁技术,当多个线程尝试使用CAS同时更新同一个变量时,只有其中一个线程能更新变量的值,而其它线程都失败,失败的线程并不会被挂起,而是被告知此次竞争中失败,并能够再次尝试。

在JDK1.5 中新增java.util.concurrent(J.U.C)就是创建在CAS之上的。相对于对于synchronized这种阻塞算法,CAS是非阻塞算法的一种常见实现。因此J.U.C在性能上有了很大的提高。

借助CAS(AtomicReference)实现单例模式:

public class Singleton {
    private static final AtomicReference<Singleton> INSTANCE = new AtomicReference<Singleton>(); 

    private Singleton() {}

    public static Singleton getInstance() {
        for (;;) {
            Singleton singleton = INSTANCE.get();
            if (null != singleton) {
                return singleton;
            }

            singleton = new Singleton();
            if (INSTANCE.compareAndSet(null, singleton)) {
                return singleton;
            }
        }
    }
}
复制代码

用CAS的好处在于不须要使用传统的锁机制来保证线程安全,CAS是一种基于忙等待的算法,依赖底层硬件的实现,相对于锁它没有线程切换和阻塞的额外消耗,能够支持较大的并行度。

使用CAS实现单例只是个思路而已,只是拓展一下帮助读者熟练掌握CAS以及单例等知识、千万不要在代码中使用!!!这个代码其实有很大的优化空间。聪明的你,知道以上代码存在哪些隐患吗?

相关文章
相关标签/搜索
本站公众号
   欢迎关注本站公众号,获取更多信息