设计模式-创建型-单例模式

150 阅读4分钟

简介

单例模式,顾名思义是一个类只有一个实例,在应用中,一般是通过getInstance()方法来获取其实例。此设计模式的好处是不需要大量创建不必要的实例,节省系统开销。该模式是属于创建型模式,一般有懒汉式、饿汉式、静态内部类和枚举等几种实现方式,以下就以代码的方式来说明各种方式的demo。

饿汉式

饿汉式,其明显的特征是在该类初始化的时候,就实例化了其单例对象,此方式实现最为简单,且能满足大部分的需求。其代码示例如下:

public class Demo1 {
    private static final Demo1 INSTACE = new Demo1();
    private Demo1() {}
    public static Demo1 getInstace() {
        return INSTACE;
    }
}

饿汉式,其实现虽然简单,但在类加载的时候就去实例化对象,这对于一些性能要求较高的系统会有瑕疵,因此就出现了懒汉式。

懒汉式

懒汉式,又称DCL(Double Check Lock)实现方式。相对于饿汉式,其在类加载的时候不会去实例化对象,而是在需要的时候再去实例化。其实现相对于饿汉式要复杂些,需要注意的细节很多,下面首先给出实现代码示例,然后再分析其中的细节问题。

public class Demo2 {
    private static volatile Demo2 INSTANCE;
    private Demo2() {}

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

需要注意的细节:

  • 在申明对象的实例时需要加上volatile关键字,禁止在创建对象的时候进行指令重排序(volatile还有一个作用是保证可见性,但并不能保证原子性)。
  • 在getInstance()时,一定要做两次非null判断,这样做的目的是防止多线程时创建多个实例。

静态内部类

使用静态内部类实现就是在类中声明一个静态内部类,然后在类中声明单例对象,最后在getInstance()方法中返回单例对象,以下是代码实例:

public class Demo3 {
    private Demo3() {}

    public static Demo3 getInstance() {
        return INNER.INSTANCE;
    }

    static class INNER {
        private static final Demo3 INSTANCE = new Demo3();
    }
}

枚举实现

该方式是《Effective Java》书中推荐的方式,其实现也比较简单,示例代码如下:

public enum Demo4 {
    INSTANCE;

    public String doSomthing() {
        return "hello world";
    }
}

测试

各种单例模式的测试如下:

public class Test {
    public static void main(String[] args) {
        Demo1 d11 = Demo1.getInstace();
        Demo1 d12 = Demo1.getInstace();
        System.out.println(d11 == d12); // true
        //-------------------------------
        Demo2 d21 = Demo2.getInstance();
        Demo2 d22 = Demo2.getInstance();
        System.out.println(d21 == d22); // true
        //-------------------------------
        Demo3 d31 = Demo3.getInstance();
        Demo3 d32 = Demo3.getInstance();
        System.out.println(d31 == d32); // true
        //-------------------------------
        Demo4 d41 = Demo4.INSTANCE;
        Demo4 d42 = Demo4.INSTANCE;
        System.out.println(d41 == d42); // true
        System.out.println(Demo4.INSTANCE.doSomthing()); // "hello world"
    }
}

单例模式深扒

单例模式安全吗

对于饿汉式、懒汉式和静态内部类,当前的实现方式有风险吗?答案是肯定的,以饿汉式为例:

import java.lang.reflect.Constructor;

public class Demo1 {
    private static final Demo1 INSTACE = new Demo1();
    private Demo1() {}
    public static Demo1 getInstace() {
        return INSTACE;
    }

    public static void main(String[] args) throws Exception {
        Demo1 instance = Demo1.getInstace();
        Constructor<Demo1> constructor = Demo1.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        Demo1 newInstance = constructor.newInstance();
        System.out.println(instance.hashCode()); //356573597
        System.out.println(newInstance.hashCode()); //1735600054
    }
}

可以明显看出,通过反射新建的实例并不等于最开始的实例,其破坏了单例。我们可以在私有构造函数中做一个判断来解决此问题,代码如下:

import java.lang.reflect.Constructor;

public class Demo1 {
    private static final Demo1 INSTACE = new Demo1();
    private Demo1() {
        synchronized (Demo1.class) {
            if (null != INSTACE) {
                throw new RuntimeException("singleton exception");
            }
        }
    }
    public static Demo1 getInstace() {
        return INSTACE;
    }

    public static void main(String[] args) throws Exception {
        Demo1 instance = Demo1.getInstace();
        Constructor<Demo1> constructor = Demo1.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        Demo1 newInstance = constructor.newInstance();
        System.out.println(instance.hashCode()); //356573597
        System.out.println(newInstance.hashCode()); //1735600054
    }
}

懒汉式的双重检测中volatile

volatile的作用是内存可见性、禁止指令重排序,但不能保证原子性,如果不加volatile,那么在多线程环境下,执行new Demo2()会有指令重排的风险。 对于new Demo2(),该操作并不是一个原子操作,会有如下三个步骤: 1)分配内存空间; 2)执行构造方法,初始化对象; 3)把对象指向这个空间。 当执行到第3)时,instace就不为null。如果不禁止指令重排序,假设有T1和T2两个线程,T1线程执行new Demo2()时,执行的顺序不是1-2-3,而是1-3-2,当执行到第3步时,此时instance已经不为null,此时还没有执行2)而T2线程进来了,T2在第一个非null判断时发现instance已经非null就直接返回instance,然而此时的instance还没有实例化完成,因此就出现了问题,所以必须加volatile。