如何写好一个单例

如何写好一个单例

[TOC]

什么是单例模式?

  • 如果你听说过设计模式,那么肯定知道单例模式,因为单例模式是设计模式中最简单的一种。顾名思义:单例模式就是一个类只有一个实例变量的一种设计模式,通过使用单例模式,可以节约系统的资源开销,避免共享资源的多重占用等优点。
  • 什么时候会用:
    1. 对于那种经常实例化但是过一会儿就被销毁的对象适合使用单例模式。
    2. 对于创建对象需要消耗很多资源的对象。如:数据库连接池对象,线程池对象等
    3. 只需要一个对象保证全局的一致性的。如:Android中Application对象,网站的计数器等。

实现一个单例模式

  • 如果你是一位对设计模式略有接触的新手,一定会毫不费力的就写出了以下单例代码(懒汉式单例:等到需要时再实例化):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    /**
    * Created by forever on 2017/9/20.
    */
    public class Singleton {
    private Singleton singleton;
    private Singleton() {
    }
    public Singleton getInstance() {
    if (singleton == null) {
    singleton = new Singleton();
    }
    return singleton;
    }
    }

    其实上面的代码就已经涵盖了单例模式最重要的三个要素:

    1. 将构造方法私有化(保证外部不能直接构造)。
    2. 有一个静态属性指向实例
    3. 提供一个公有的静态方法向外面提供这个实例。
  • 然而表面看似完美的代码,内部其实暗藏杀鸡:

    这里写图片描述

    在单线程中看似是没有什么问题的,但如果放在多线程的环境中就会有问题了。加入有两个线程同时访问getInstance方法,如果期中一个线程刚进入if (singleton == null){}里面,这个时候另一个线程恰好也访问这个方法,并且完成创建了一个实例,那个刚刚挂起的那个线程继续运行的话就会再创建一个实例。那我们单例的理想不就破灭的了嘛。

    这里写图片描述

基本方法的改进

  • 既然了解了问题,那么我们如何才能防止两个线程同时实例化方法呢?有经验的同学或许就会立刻想到了Java的同步。通过synchronized关键字进行加锁。

    1
    2
    3
    4
    5
    6
    public synchronized Singleton getInstance() {
    if (singleton == null) {
    singleton = new Singleton();
    }
    return singleton;
    }
  • 不过加锁的话对程序的性能性能会有很大影响,如果当某个线程正在访问该方法的时候其他线程就只能在锁池中等待该线程释放锁,我们稍加改进一下:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    public Singleton getInstance() {
    if (singleton == null) {
    synchronized (Singleton.class){
    if(singleton == null){
    singleton = new Singleton();
    }
    }
    }
    return singleton;
    }

    这样只在构造实例代码的时候加锁,对程序的性能影响就小多了。而且只要实例化完成之后,后面基本就不会进入这个同步代码块了。

  • 看似已经很完美了,那么我们有什么其他办法不用加锁的方式也能避免多线程的问题呢?ok,当然有的,我们可以使用饿汉式的单例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    /**
    * Created by forever on 2017/9/20.
    */
    public class Singleton {
    public Singleton singleton = new Singleton();
    private Singleton() {
    }
    public Singleton getInstance() {
    return singleton;
    }
    }

    上面代码与懒汉式加载最大的区别在于这里的single在开始就实例化了,也就是不管我们是否使用它,都会将其加载到内存中去。这个在获取的时候就直接返回就行了。如果不在意内存的话最好使用这个方法。

  • 如果你说,我既不想要使用同步,但又十分在意内存资源怎么办,ok,说明你是一个很有追求的人,其实在也是有办法的(办法总比问题多):

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    public class Singleton {
    private Singleton(){
    }
    public static Singleton getInstance(){
    return Nested.singleton;
    }
    public static class Nested{
    static {
    System.out.println("蛤蛤");
    }
    private static Singleton singleton = new Singleton();
    }
    }

    这个时候我们就需要一个内部类作为桥梁了,当我们getInstance()时,类加载器才会去加载Nested,然后实例化Singleton的实例,如果你对Java的类加载机制有了解的话一定很容易就理解了上述代码。

总结

  • Java的单例模式看似简单,其实深究而言还是有很多值得深究的东西的,如果在面试的碰到了也可以和面试官多吹一会儿。最近一直在准备校招,作为菜鸟表示压力山大,也祝愿大家都能找到满意的工作。如果发现写的有什么问题欢迎指正,希望与大家共同进步。