设计模式之单例设计模式

图1

单例模式可以说是我们开发中用得最多的一种创建型设计模式了。当我们想控制某个类在内存中存在对象的个数;一个系统可以同时存在多个打印的任务,但是同时只能有一个对象被打印;一个系统只能有一个窗口或者一个资源管理器;一个程序只能有一个计数器时….我们都需要使用单例模式来解决这类问题。

我们常用的单例有饿汉式,懒汉式,DCL(双重效验锁),枚举实现和静态内部类这5种实现方式。

饿汉式

饿汉式,故名思义,因为饿嘛,所以在类被加载到内存的时候,对象就已经被创建好了,而不管是否马上需要使用它。其生命周期与类保持一致。

饿汉式通用写法

下面是饿汉式的通用写法:

public class SingleTon {
    public static final SingleTon single = new SingleTon();

    private SingleTon() {
    }

    public static SingleTon getInstance() {
        return single;
    }
}

饿汉式的缺点

  1. 在类加载时,完成了对象的初始化,所以类的加载稍慢,但对象的加载是非常快的,是一个以空间换时间的做法。
  2. 当类加载完成时,对象也就被创建并缓存在内存里了,不管是否需要使用,不满足按需使用的原则。

懒汉式

懒汉式,因为它懒,所以,在类被加载的时候,并没有立刻创建对象,而是当需要时,调用相应方法时,才会创建对象,与饿汉式刚好相反。类的加载比较快,而对象的加载会慢点,因为要在调用方法时,才会去new对象。

懒汉式的通用写法

public class SingleTon {
    public static SingleTon single = null;

    private SingleTon() {
    }

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

懒汉式的缺点

  1. 懒汉式是以时间换空间的一种做法,真正需要的时候,才去创建对象,创建对象也是需要时间的,所以,会比饿汉式获取对象的时候稍慢。
  2. 多线程访问是不安全的,因为创建对象时需要时间的,正在创建对象的时候,如果有其它线程也执行了getInstance方法,而对象还没有创建成功的话,上面的判断就失去了作用,而让其它线程也创建了该类的实现。
  3. 对于2的做法可以对方法使用synchronized方法同步的方式,来做线程同步,而这样做,效率比较低。

DCL(双重效验锁机制)

在懒汉式的基础上做了对多线程的支持,通过两重判断和同步代码块来保证多线程的同步问题。99.99%的多线程环境下,都不会有问题。而如果出现问题,也是很难重现,以及排查错误的。

DCL的通用写法

public class SingleTon {
    public static SingleTon single = null;

    private SingleTon() {
    }

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

这里的两层判断分别代码的意思:

  1. 第一层判断主要是为了避免不必要的同步。也就是说当对象已经创建了,就不必再执行同步代码块来消耗不必要的时间了。
  2. 第二层主要是为了当对象为null时创建该对象的实例。

DCL的缺点

  1. DCL并没有明显的缺点,它是一个综合的结果。但是在高并发的情况下,会有一定机率出现问题。

为什么DCL的写法存在问题

我们先来说说创建对象时,jvm的大致工作。

  1. 给SingleTon的实例分配内存。
  2. 初始化SingleTon的构造器。
  3. 将instance对象指向分配的内存空间

由于java编译器允许处理器乱序执行(out-of-order),以及JDK1.5之前JMM(java memory model)Cache、寄存器到主内存回写的顺序,上面的第二点和第三点无法得到保证。好就是说,有可能执行的顺序是123,或者132。而如果是执行132的话,当一个线程执行的顺序是132时,当它执行到了3,这时,instance对象已经指定了1中分配的地址,已经不为null了,而此时另一个线程正好执行同样的代码时,因为instance已经不为null了,所以,就直接使用了此对象,结果,因为此对象根本还未创建完毕,而导致报错。

这样的问题是很难被发现的。实际上在java1.5之后,调整了JMM,具体化的volatile关键字。因此只要在java1.5之后,将instance定义成private volatile static SingleTon singleTon = null.就可以保证每次获取instance都去主内存里读取,就可以解决DCL带来的问题。

优化后的DCL代码

public class SingleTon {
    public volatile static SingleTon single = null;

    private SingleTon() {
    }

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

静态内部类实现

静态内部类的实现,实际上是对DCL的另一种写法,它规避了DCL可能带来的潜在麻烦,而又拥有DCL按需加载,以及多线程安全的优点。是一种比DCL更好的实现方式。一般会推荐此写法。

静态内部类通用写法

public class SingleTon {

    private static class Inner {
        public static final SingleTon single = new SingleTon();
    }

    private SingleTon() {
    }

    public static SingleTon getInstance() {

        return Inner.single;
    }
}

静态内部类的缺点

反射的问题

以上几种实现,实际上都不能保证对象在内存中的唯一性,因为有反射的存在。当使用反射时,上面的实现都将失去其作用。

如果使用上面的方法使用单例,只能靠代码规范来约束使用者了。而如果只在内部使用的话,我们通过代码规范一般是可行的。但如果是对外的第三方接入系统的话,可能有一定机率出现问题。

那有没有一种真正的能保证内存中单一实例的实现方式呢?有,使用枚举。

枚举实现单例

由于枚举的实现问题,造就了其天然支持多线程并发的特性,我们用其来实现单例是最好不过的事情了。

public enum SingleEnum {
    SingleTon {
        private int a = 0;

        @Override
        public SingleEnum getSingleTon() {
            return SingleTon;
        }


        /**
         * 测试方法
         */
        public void testMethod() {
        }
    };

    public abstract SingleEnum getSingleTon();

    private SingleEnum() {
    }
}

每一个枚举变量,实际上都是由类所组成的。而每个类里面即可以写变量,也可以写方法。所以,使用枚举来实现单例,也是一个不错的做法。

关于单例这种设计模式的几种写法,就写到这里了,在这里我推荐使用第四种静态内部类的方式来实现单例,而高并发的极端情况,如果不嫌麻烦,也可以使用枚举的方式来实现。