设计模式之单例模式

229 阅读8分钟

1.简介:

单例模式(Singleton Pattern)是Java中最简单的设计模式之一。这种类型的设计模式属于创建型模式,它提供了一种创建对象的最佳方式。 这种模式涉及到一个单一的类,该类负责创建自己的对象,同时确保只有单个对象被创建。这个类提供了一种访问其唯一的对象的方式,可以直接访问,不需要实例化该类的对象。

注意:

  • 1.单例类只能有一个实例。
  • 2.单例类必须自己创建自己的唯一实例。
  • 3.单例类必须给所有其他对象提供这一实例。

2.介绍:

意图:保证一个类仅有一个实例,并提供一个访问它的全局访问点。
主要解决:一个全局使用的类频繁地创建与销毁。
何时使用:当您想控制实例数目,节省系统资源的时候。
如何解决:判断系统是否已经有这个单例,如果有则返回,如果没有则创建。
关键代码:构造函数是私有的。

优点:

  • 1.在内存里只有一个实例,减少了内存的开销,尤其是频繁的创建和销毁实例。
  • 2.避免对资源的多重占用(比如写文件操作)。

缺点:

  • 1.没有接口,不能继承,与单一职责原则冲突,一个类应该只关心内部逻辑,而不关心外面怎么样来实例化。

使用场景:

  • 1.要求生产唯一序列号。
  • 2.WEB 中的计数器,不用每次刷新都在数据库里加一次,用单例先缓存起来。
  • 3.创建的一个对象需要消耗的资源过多,比如 I/O 与数据库的连接等。

3.单例模式的实现方式:

3.1:懒汉式(线程不安全)

3.1.1 描述:

把自己的构造方法设置为private的,不让别人new你的实例,提供一个static方法给别人获取你的实例,在这个方法里面返回创建的实例,在使用的时候才创建

3.1.2 实例:

public class Singleton {  
    private static Singleton instance;  
    private Singleton (){}  
  
    public static Singleton getInstance() {  
        if (instance == null) {  
           instance = new Singleton();  
        }  
        return instance;  
    }  
}

3.2:饿汉式(线程安全)

3.2.1 描述:

把自己的构造方法设置为private的,不让别人new你的实例,提供一个static方法给别人获取你的实例,在这个方法里面返回创建的实例,类加载时就初始化

3.2.2 实例:

public class Singleton { 
    private static Singleton instance = new Singleton();  
    private Singleton (){}  
    public static Singleton getInstance() {  
        return instance;  
    }  
}

3.3:线程安全(synchronized)

3.3.1 描述:

当有多个线程并发执行getInstance()的时候,可能会出现以下的情况而导致Singleton产生多个实例。
线程一 : Singleton.getInstance()
(Singleton:判断singleton为null,进行singleton实例的初始化)

线程二: Singleton.getInstance()
(Singleton: singleton还没初始化完,依然为null,于是进行另外一个singleton实例的初始化)
等到两个线程都返回的时候,其实是创建了两个Singleton的实例, 当有两个线程同时执行getInstance()方法的时候,一旦线程一获取到Singleton.class锁,线程二只能在外面等待着。

3.3.2 实例:

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

3.4:双检锁/双重校验锁(DCL,即 double-checked locking)

3.4.1 描述:

  • 双重检验中的第一次检验:当有多有线程调用getInstance()方法的时候,,不管三七二十一,先让他们进来。如果singleton实例不为空,那最好了,直接return实例singleton,跟synchronized一点都扯不上关系,所以也不会影响到性能。
  • 双重检验中的第二次检验:当有多个线程通过第一次检验时,假设线程拿到锁进入synchronized语句块,对singleton实例进行初始化,释放Singleton.class锁之后,线程二持有这个锁进入synchronized语句块,此时又对singleton对象就行初始化。
  • 假设线程一进入第二次检验之后就执行Singleton instance = new Singleton(); 操作,在这个操作中,JVM主要干了三件事:
    1、在堆空间里分配一部分空间;
    2、执行Singleton的构造方法进行初始化;
    3、把instance对象指向在堆空间里分配好的空间.
    但是,当我们编译的时候,编译器在生成汇编代码的时候会对流程顺序进行优化。优化的结果是有可能按照1-2-3顺序执行,也可能按照1-3-2顺序执行。

我们知道,执行完3的时候就singleton对象就已经不为空了,如果是按照1-3-2的顺序执行,恰巧在执行到3的时候(还没执行2),突然跑来了一个线程,进来getInstance()方法之后判断singleton不为空就返回了singleton实例。

此时singleton实例虽不为空,但它还没执行构造方法进行初始化。又恰巧构造方法里面需要对某些参数进行初始化。后来闯进来的线程糊里糊涂对那些需要初始化的参数进行操作就有可能报错奔溃了。

volatile 的特性:

  • 保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。(实现可见性)
  • 禁止进行指令重排序。(实现有序性)
  • volatile 只能保证对单次读/写的原子性。i++ 这种操作不能保证原子性。

3.4.2 实例:

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;  
    }  
}

3.5:静态内部类

3.5.1 描述:

利用JVM的机制完成的,但并不适用其他语言,这种方式是 Singleton 类被装载了,instance 不一定被初始化,只有被调用 getInstance 方法时才会装载。

3.5.2 实例:

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

3.6:枚举方式:

3.6.1 描述:

枚举实际上就是一个继承Enum的类。所以本质还是一个类,因为枚举的特点,只会有一个实例,同时保证了线程安全、反射安全和反序列化安全。

3.6.2 实例:

public enum Singleton {  
    INSTANCE;  
    public Singleton getInstance() {  
        return INSTANCE;
    }  
}

4.用枚举类型实现单例模式(避免反射,序列化问题)

4.1:私有化构造器并不保险:

采用双检锁/双重校验锁(DCL,即 double-checked locking)的单例模式,代码如下:

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;
    }

    public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
        Singleton s=Singleton.getSingleton();
        Singleton sUsual=Singleton.getSingleton();
        Constructor<Singleton> constructor=Singleton.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        Singleton sReflection=constructor.newInstance();
        System.out.println(s+"\n"+sUsual+"\n"+sReflection);
        System.out.println("正常情况下,实例化两个实例是否相同:"+(s==sUsual));
        System.out.println("通过反射攻击单例模式情况下,实例化两个实例是否相同:"+(s==sReflection));
    }
}

执行结果如下:

4.2:序列化问题:

先看代码如下:

public class SerSingleton implements Serializable {
    private volatile static SerSingleton uniqueInstance;
    private  String content;
    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }
    private SerSingleton() {
    }

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


    public static void main(String[] args) throws IOException, ClassNotFoundException {
        SerSingleton s = SerSingleton.getInstance();
        s.setContent("单例序列化");
        System.out.println("序列化前读取其中的内容:"+s.getContent());
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("SerSingleton.obj"));
        oos.writeObject(s);
        oos.flush();
        oos.close();

        FileInputStream fis = new FileInputStream("SerSingleton.obj");
        ObjectInputStream ois = new ObjectInputStream(fis);
        SerSingleton s1 = (SerSingleton)ois.readObject();
        ois.close();
        System.out.println(s+"\n"+s1);
        System.out.println("序列化后读取其中的内容:"+s1.getContent());
        System.out.println("序列化前后两个是否同一个:"+(s==s1));
    }
}

执行结果:

可以看出,序列化前后两个对象并不想等。简单来说“任何一个readObject方法,不管是显式的还是默认的,它都会返回一个新建的实例,这个新建的实例不同于该类初始化时创建的实例”。

但是如何让序列化和反序列化使用同一个实例呢?

可以增加readResolve()方法来预防:

这是反序列化机制决定的, 在反序列化的时候会判断如果实现了serializable 或者 externalizable接口的类中又包含readResolve()方法的话,会直接调用readResolve()方法来获取实例

public class SerSingleton implements Serializable {
    private volatile static SerSingleton uniqueInstance;
    private  String content;
    public String getContent() {
        return content;
    }

    public void setContent(String content) {
        this.content = content;
    }
    private SerSingleton() {
    }

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

    private Object readResolve(){
        if (uniqueInstance == null) {
            synchronized (SerSingleton.class) {
                if (uniqueInstance == null) {
                    uniqueInstance = new SerSingleton();
                }
            }
        }
        return uniqueInstance;
    }


    public static void main(String[] args) throws IOException, ClassNotFoundException {
        SerSingleton s = SerSingleton.getInstance();
        s.setContent("单例序列化");
        System.out.println("序列化前读取其中的内容:"+s.getContent());
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("SerSingleton.obj"));
        oos.writeObject(s);
        oos.flush();
        oos.close();

        FileInputStream fis = new FileInputStream("SerSingleton.obj");
        ObjectInputStream ois = new ObjectInputStream(fis);
        SerSingleton s1 = (SerSingleton)ois.readObject();
        ois.close();
        System.out.println(s+"\n"+s1);
        System.out.println("序列化后读取其中的内容:"+s1.getContent());
        System.out.println("序列化前后两个是否同一个:"+(s==s1));
    }
}

执行结果如下:

有没有其他方式可以解决序列反序列的问题呢?

对于实例控制,枚举类型优于readResolve。

4.3:枚举类详解:

4.3.1:检验枚举的安全问题(避免反射攻击):

同样通过反射修改构造器创建实例:

public enum  EnumSingleton {
    INSTANCE;
    public EnumSingleton getInstance(){
        return INSTANCE;
    }

    public static void main(String[] args) throws IllegalAccessException, InvocationTargetException, InstantiationException, NoSuchMethodException {
        EnumSingleton singleton1=EnumSingleton.INSTANCE;
        EnumSingleton singleton2=EnumSingleton.INSTANCE;
        System.out.println("正常情况下,实例化两个实例是否相同:"+(singleton1==singleton2));
        Constructor<EnumSingleton> constructor= null;
        constructor = EnumSingleton.class.getDeclaredConstructor();
        constructor.setAccessible(true);
        EnumSingleton singleton3= null;
        singleton3 = constructor.newInstance();
        System.out.println(singleton1+"\n"+singleton2+"\n"+singleton3);
        System.out.println("通过反射攻击单例模式情况下,实例化两个实例是否相同:"+(singleton1==singleton3));
    }
}

执行结果:

4.3.2:枚举类序列和反序列问题:

public enum  SerEnumSingleton implements Serializable {
    INSTANCE;
    private  String content;
    public String getContent() {
        return content;
    }
    public void setContent(String content) {
        this.content = content;
    }
    private SerEnumSingleton() {
    }

    public static void main(String[] args) throws IOException, ClassNotFoundException {
        SerEnumSingleton s = SerEnumSingleton.INSTANCE;
        s.setContent("枚举单例序列化");
        System.out.println("枚举序列化前读取其中的内容:"+s.getContent());
        ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("SerEnumSingleton.obj"));
        oos.writeObject(s);
        oos.flush();
        oos.close();

        FileInputStream fis = new FileInputStream("SerEnumSingleton.obj");
        ObjectInputStream ois = new ObjectInputStream(fis);
        SerEnumSingleton s1 = (SerEnumSingleton)ois.readObject();
        ois.close();
        System.out.println(s+"\n"+s1);
        System.out.println("枚举序列化后读取其中的内容:"+s1.getContent());
        System.out.println("枚举序列化前后两个是否同一个:"+(s==s1));
    }
}

执行结果:

对于反射攻击和序列反序列,既然能解决这些问题,还能使代码量变的极其简洁,那我们就有理由选枚举单例模式了。


参考文章:

  1. 码农翻身之Java帝国之单例设计模式
  2. 菜鸟教程之单例模式
  3. 为什么要用枚举实现单例模式(避免反射、序列化问题)