Gof23-Singleton模式

1. 模式动机

对于系统中的某些类来说,只有一个实例很重要,例如,一个系统中可以存在多个打印任务,但是只能有一个正在工作的任务;一个系统只能有一个窗口管理器或文件系统;一个系统只能有一个计时工具或ID(序号)生成器。

如何保证一个类只有一个实例并且这个实例易于被访问呢?定义一个全局变量可以确保对象随时都可以被访问,但不能防止我们实例化多个对象。

一个更好的解决办法是让类自身负责保存它的唯一实例这个类可以保证没有其他实例被创建,并且它可以提供一个访问该实例的方法。这就是单例模式的模式动机。

为了达到这个目的,必须设置构造器私有。

2. 模式定义

单例模式(Singleton Pattern):单例模式确保某一个类只有一个实例,而且自行实例化并向整个系统提供这个实例,这个类称为单例类,它提供全局访问的方法

单例模式的要点有三个:

  • 一是某个类只能有一个实例;
  • 二是它必须自行创建这个实例;
  • 三是它必须自行向整个系统提供这个实例。

单例模式是一种对象创建型模式。单例模式又名单件模式或单态模式。

3. 模式结构

单例模式只有角色:

  • Singleton:单例

4. 模式分析

单例模式的目的是保证一个类仅有一个实例,并提供一个访问它的全局访问点。单例模式包含的角色只有一个,就是单例类——Singleton。单例类拥有一个私有构造函数,确保用户无法通过new关键字直接实例化它。除此之外,该模式中包含一个静态私有成员变量与静态公有的工厂方法,该工厂方法负责检验实例的存在性并实例化自己,然后存储在静态成员变量中,以确保只有一个实例被创建。

在单例模式的实现过程中,需要注意如下三点:

  • 单例类的构造函数为私有;
  • 提供一个自身的静态私有成员变量;
  • 提供一个公有的静态工厂方法。

5. 分类

5.1. 饿汉单例模式

饿汉单例模式是一种常见的单例模式,它在类加载时就创建了单例对象,因此也被称为“饿汉模式”。这种模式的优点是实现简单,线程安全,但缺点是可能会浪费一些内存空间,因为即使不需要使用该单例对象,它也会被创建。

public class Hungry {

    // 当这个对象拥有一个占用内存特别大的对象,那么会浪费很大一块内存空间
    private byte[] buffer = new byte[1024*100];

    static {
        System.out.println("饿汉单例初始化,即将创建单例对象");
    }
    // 在类加载的初始化阶段时,就创建实例
    private final static Hungry instance = new Hungry();

    // 设置构造器私有
    private Hungry() {
        System.out.println("饿汉单例执行构造方法");
    }

    public static Hungry getInstance() {
        return instance;
    }

    public static void main(String[] args) {
        Hungry instance = Hungry.getInstance();
        System.out.println(instance);
    }
}
/**
 * 运行结果:
 * 饿汉单例初始化,即将创建单例对象
 * 饿汉单例执行构造方法
 * basic.designpattern.singleton.Hungry@1b6d3586
 */

5.2. 懒汉单例模式

懒汉单例模式在第一次调用 getInstance 方法时,实例化 LazySingleton 对象,在类加载时并不自行实例化,这种技术称为延迟加载技术(Lazy Load) ,即在需要的时候进行加载实例。

这种模式的优点是节省内存空间,缺点是可能有线程安全的问题

单线程下,代码安全:

public class LazySingleton {
    private static LazySingleton instance;

    static {
        System.out.println("LazySingleton 初始化...");
    }

    // 构造器私有
    private LazySingleton() {
        System.out.println("LazySingleton 执行构造方法");
    }

    // 使用时才实例化对象
    public static LazySingleton getInstance() {
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }

    public static void main(String[] args) {
        LazySingleton instance = LazySingleton.getInstance();
        LazySingleton instance2 = LazySingleton.getInstance();
        System.out.println(instance);
        System.out.println(instance2);
    }
}
/**
 * 执行结果:
 * LazySingleton 初始化...
 * LazySingleton 执行构造方法
 * basic.designpattern.singleton.LazySingleton@1b6d3586
 * basic.designpattern.singleton.LazySingleton@1b6d3586
 */

可以看到只调用了一次构造方法,并且两个对象是相同的。

多线程环境下调用:

public class LazySingleton {
    private static LazySingleton instance;

    static {
        System.out.println("LazySingleton 初始化...");
    }

    // 构造器私有
    private LazySingleton() {
        System.out.println(Thread.currentThread().getName() + "LazySingleton 执行构造方法");
    }

    public static LazySingleton getInstance() {
        if (instance == null) {
            instance = new LazySingleton();
        }
        return instance;
    }

    public static void main(String[] args) {

        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                LazySingleton.getInstance();
            }).start();
        }
    }
}
/**
 * 执行结果:
 * LazySingleton 初始化...
 * Thread-0LazySingleton 执行构造方法
 * Thread-2LazySingleton 执行构造方法
 * Thread-1LazySingleton 执行构造方法
 */

可以看到,当在多线程环境下,可能多个线程同时调用getInstance()方法,并且同时调用了构造方法,导致生成多个实例对象。这就不满足单例了。

所以代码需要优化。

5.3. DCL双重检测锁懒汉模式

因为懒汉模式在多线程环境下不安全,无法满足单例,所以需要对代码进行优化加锁处理。

DCL懒汉模式是指在懒汉模式的基础上,加入了双重检测机制,保证了线程安全,同时也保证了效率。

DCL懒汉模式的实现方式是:在 getInstance() 方法中进行双重检查,第一次检查 instance 是否为 null,如果为 null,则进入同步代码块,再次检查 instance 是否为 null,如果还是 null,则创建实例。

DCL懒汉模式相对于懒汉模式来说,在多线程环境下更加安全,同时也不会影响程序的性能。

public class DCLLazySingleton {
    private static DCLLazySingleton instance;

    static {
        System.out.println("LazySingleton 初始化...");
    }

    // 构造器私有
    private DCLLazySingleton() {
        System.out.println(Thread.currentThread().getName() + "LazySingleton 执行构造方法");
    }

    public static DCLLazySingleton getInstance() {
        // 第一重检测,如果instance为空,则进行加锁
        if (instance == null) {
            synchronized (DCLLazySingleton.class) {
                // 第二重检测,加锁完毕后,继续检测instance是否为空,如果为空才进行对象实例化
                if (instance == null) {
                    instance = new DCLLazySingleton(); // 不是原子化操作
                }
            }
        }
        return instance;
    }

    public static void main(String[] args) {

        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                DCLLazySingleton.getInstance();
            }).start();
        }
    }
}
/**
 * 执行结果:
 * LazySingleton 初始化...
 * Thread-0LazySingleton 执行构造方法
 */

这个代码在极端环境下依旧可能出现错误,在JDK1.5之前,由于Java内存模型中存在缺陷,可能会导致DCL懒汉模式失效。 这个缺陷是由于Java内存模型中存在的指令重排问题导致的。

在JDK1.5之后,Java内存模型得到了改进,DCL懒汉模式已经可以正常工作了。

为了解决这个问题,可以使用volatile关键字来修饰instance变量,保证其可见性和有序性

什么是指令重排?

指令重排是指编译器或CPU为了优化程序的执行性能而对指令进行重新排序的一种手段。 重排序会带来可见性问题,所以在多线程开发中必须要关注并规避重排序。

Java中,使用new关键字创建对象并不是一个原子性操作,它会经历这三个指令

  1. 分配内存空间
  2. 执行构造方法,初始化对象
  3. 把对象指向这块空间

如果CPU对这三个指令进行重拍,可能会有这样的顺序 1 -> 3 -> 2

回到刚刚的代码:

当线程A执行到同步代码块,执行new关键字实例化对象,此时CPU对new进行了指令重拍,1 -> 3 -> 2

线程A执行到3这个指令,instance变量已经指向一块空间,但对象还未初始化,此时线程B发现instance变量不为空,getInstacne()方法就直接返回了一个空对象。这就出现了线程安全问题。

为了解决这个问题,可以使用volatile关键字来修饰instance变量,保证其可见性和有序性

volatile关键字:

在Java中,volatile关键字是一种轻量级的同步机制,它可以保证可见性和有序性。当一个变量被volatile修饰后,表示着线程本地内存无效,当一个线程修改共享变量后,它会立即被更新到主内存中,其他线程读取共享变量时,会直接从主内存中读取。

volatile关键字可以禁止指令重排,这是因为volatile关键字包含“禁止指令重排序”的语义

public class DCLLazySingleton {
    private volatile static DCLLazySingleton instance;

    static {
        System.out.println("LazySingleton 初始化...");
    }

    // 构造器私有
    private DCLLazySingleton() {
        System.out.println(Thread.currentThread().getName() + "LazySingleton 执行构造方法");
    }

    public static DCLLazySingleton getInstance() {
        // 第一重检测,如果instance为空,则进行加锁
        if (instance == null) {
            synchronized (DCLLazySingleton.class) {
                // 第二重检测,加锁完毕后,继续检测instance是否为空,如果为空才进行对象实例化
                if (instance == null) {
                    instance = new DCLLazySingleton(); // 不是原子化操作
                    /**
                     * 1.分配内存空间
                     * 2.执行构造方法,初始化对象
                     * 3.把对象指向这块空间
                     */
                }
            }
        }
        return instance;
    }

    public static void main(String[] args) {

        for (int i = 0; i < 10; i++) {
            new Thread(()->{
                DCLLazySingleton.getInstance();
            }).start();
        }
    }
}
/**
 * 执行结果:
 * LazySingleton 初始化...
 * Thread-0LazySingleton 执行构造方法
 */

5.4. 静态内部类实现单例模式

静态内部类实现单例模式是一种常见的单例模式实现方式。在Java中,静态内部类不会在外部类加载时被加载只有在第一次使用时才会被加载,这种方式可以保证线程安全性和延迟加载。

public class HolderSingleton {

    static {
        System.out.println("HolderSingleton类加载中,初始化....");
    }
    public HolderSingleton() {
        System.out.println("HolderSingleton构造方法执行");
    }

    public static Inner getInstance() {
        return Inner.instance;
    }

    /**
     * 静态内部类不会在外部类加载时被加载,只有在第一次使用时才会被加载
     */
    private static class Inner {
        static {
            System.out.println("Inner类加载中,初始化....");
        }
        private static Inner instance = new Inner();
        // 私有构造方法
        private Inner() {
            System.out.println("调用静态内部类构造方法");
        }
    }

    public static void main(String[] args) {
        Inner instance = HolderSingleton.getInstance();
        Inner instance1 = HolderSingleton.getInstance();

        System.out.println(instance1);
        System.out.println(instance);

    }
}
/**
 * HolderSingleton类加载中,初始化....
 * Inner类加载中,初始化....
 * 调用静态内部类构造方法
 * basic.designpattern.singleton.HolderSingleton$Inner@1b6d3586
 * basic.designpattern.singleton.HolderSingleton$Inner@1b6d3586
 */

5.4. 使用反射破坏单例

public class Test {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
        DCLLazySingleton lazy1 = DCLLazySingleton.getInstance();

        Constructor<DCLLazySingleton> constructor = DCLLazySingleton.class.getDeclaredConstructor(null);
        // 无视构造器的private修饰
        constructor.setAccessible(true);
        DCLLazySingleton lazy2 = constructor.newInstance();

        System.out.println(lazy1);
        System.out.println(lazy2);
    }
}
/**
 * LazySingleton 初始化...
 * mainLazySingleton 执行构造方法
 * mainLazySingleton 执行构造方法
 * basic.designpattern.singleton.DCLLazySingleton@1b6d3586
 * basic.designpattern.singleton.DCLLazySingleton@4554617c
 */

如何应对:

在构造方法中加对象锁,

public class DCLLazySingleton {
    // 使用volatile关键字保证可见性和有序性
    private volatile static DCLLazySingleton instance;

    static {
        System.out.println("LazySingleton 初始化...");
    }

    // 构造器私有
    private DCLLazySingleton() {

        synchronized (DCLLazySingleton.class) {
            if (instance != null) {
                throw new RuntimeException("请不要使用反射破坏单例");
            }
        }
        System.out.println(Thread.currentThread().getName() + "LazySingleton 执行构造方法");
    }

    public static DCLLazySingleton getInstance() {
        // 第一重检测,如果instance为空,则进行加锁
        if (instance == null) {
            synchronized (DCLLazySingleton.class) {
                // 第二重检测,加锁完毕后,继续检测instance是否为空,如果为空才进行对象实例化
                if (instance == null) {
                    instance = new DCLLazySingleton(); // 不是原子化操作
                    /**
                     * 1.分配内存空间
                     * 2.执行构造方法,初始化对象
                     * 3.把对象指向这块空间
                     */
                }
            }
        }
        return instance;
    }
}

再次破坏:

public class Test {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
//        DCLLazySingleton lazy1 = DCLLazySingleton.getInstance();

        Constructor<DCLLazySingleton> constructor = DCLLazySingleton.class.getDeclaredConstructor(null);
        // 无视构造器的private修饰
        constructor.setAccessible(true);
        // 使用反射生成的构造器破坏
        DCLLazySingleton lazy2 = constructor.newInstance();
        DCLLazySingleton lazy1 = constructor.newInstance();

        System.out.println(lazy1);
        System.out.println(lazy2);

    }
}
/**
 * LazySingleton 初始化...
 * mainLazySingleton 执行构造方法
 * mainLazySingleton 执行构造方法
 * basic.designpattern.singleton.DCLLazySingleton@1b6d3586
 * basic.designpattern.singleton.DCLLazySingleton@4554617c
 */

如何应对:

通过使用红绿灯的方法,添加一个隐藏变量

public class DCLLazySingleton {
    // 使用volatile关键字保证可见性和有序性
    private volatile static DCLLazySingleton instance;

    static {
        System.out.println("LazySingleton 初始化...");
    }
    private static boolean flag = false;

    // 构造器私有
    private DCLLazySingleton() {

        synchronized (DCLLazySingleton.class) {
            if (flag == false) {
                flag = true;
            } else {
                throw new RuntimeException("请不要使用反射破坏单例");
            }
        }
        System.out.println(Thread.currentThread().getName() + "LazySingleton 执行构造方法");
    }

    public static DCLLazySingleton getInstance() {
        // 第一重检测,如果instance为空,则进行加锁
        if (instance == null) {
            synchronized (DCLLazySingleton.class) {
                // 第二重检测,加锁完毕后,继续检测instance是否为空,如果为空才进行对象实例化
                if (instance == null) {
                    instance = new DCLLazySingleton(); // 不是原子化操作
                    /**
                     * 1.分配内存空间
                     * 2.执行构造方法,初始化对象
                     * 3.把对象指向这块空间
                     */
                }
            }
        }
        return instance;
    }
}

测试:

public class Test {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException {
//        DCLLazySingleton lazy1 = DCLLazySingleton.getInstance();

        Constructor<DCLLazySingleton> constructor = DCLLazySingleton.class.getDeclaredConstructor(null);
        // 无视构造器的private修饰
        constructor.setAccessible(true);
        // 使用反射生成的构造器破坏
        DCLLazySingleton lazy2 = constructor.newInstance();
        DCLLazySingleton lazy1 = constructor.newInstance();

        System.out.println(lazy1);
        System.out.println(lazy2);

    }
}
/**
 * LazySingleton 初始化...
 * mainLazySingleton 执行构造方法
 * Exception in thread "main" java.lang.reflect.InvocationTargetException
 * 	at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
 * 	at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62)
 * 	at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45)
 * 	at java.lang.reflect.Constructor.newInstance(Constructor.java:423)
 * 	at basic.designpattern.singleton.Test.main(Test.java:21)
 * Caused by: java.lang.RuntimeException: 请不要使用反射破坏单例
 * 	at basic.designpattern.singleton.DCLLazySingleton.<init>(DCLLazySingleton.java:25)
 * 	... 5 more
 */

再次破坏:

假设知道了隐藏变量的名称:

public class Test {
    public static void main(String[] args) throws NoSuchMethodException, InvocationTargetException, InstantiationException, IllegalAccessException, NoSuchFieldException {
//        DCLLazySingleton lazy1 = DCLLazySingleton.getInstance();

        Field flag = DCLLazySingleton.class.getDeclaredField("flag");
        // 破坏flag的private修饰
        flag.setAccessible(true);

        Constructor<DCLLazySingleton> constructor = DCLLazySingleton.class.getDeclaredConstructor(null);
        // 无视构造器的private修饰
        constructor.setAccessible(true);
        // 使用反射生成的构造器破坏
        DCLLazySingleton lazy2 = constructor.newInstance();
        // 将值设置会false
        flag.set(lazy2, false);

        DCLLazySingleton lazy1 = constructor.newInstance();

        System.out.println(lazy1);
        System.out.println(lazy2);

    }
}
/**
 * LazySingleton 初始化...
 * mainLazySingleton 执行构造方法
 * mainLazySingleton 执行构造方法
 * basic.designpattern.singleton.DCLLazySingleton@74a14482
 * basic.designpattern.singleton.DCLLazySingleton@1540e19d
 */

5.5. 使用序列化破坏单例

public class TestSerializable {

    public static void main(String[] args) {
        DCLLazySingleton instance = DCLLazySingleton.getInstance();

        System.out.println("====");

        try {
            ObjectOutputStream objectOutputStream = new ObjectOutputStream(new FileOutputStream("obj.txt"));
            ObjectInputStream objectInputStream = new ObjectInputStream(new FileInputStream("obj.txt"));
            objectOutputStream.writeObject(instance);
            Object o = objectInputStream.readObject();

            System.out.println(instance);
            System.out.println(o);
            System.out.println(instance == o);

        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
/**
 * LazySingleton 初始化...
 * mainLazySingleton 执行构造方法
 * ====
 * basic.designpattern.singleton.DCLLazySingleton@27973e9b
 * basic.designpattern.singleton.DCLLazySingleton@312b1dae
 * false
 */

5.6. 枚举实现单例模式

枚举是在JDK1.5以及以后版本中增加的一个“语法糖”,它主要用于维护一些实例对象固定的类。例如一年有四个季节,就可以将季节定义为一个枚举类型,然后在其中定义春、夏、秋、冬四个季节的枚举类型的实例对象。 按照Java语言的命名规范,通常,枚举的实例对象全部采用大写字母定义,这一点与Java里面的常量是相同的。

因为Java虚拟机会保证枚举对象的唯一性,因此每一个枚举类型和定义的枚举变量在JVM中都是唯一的。

public enum EnumSingleton {
    SINGLETON;
  
    static {
        System.out.println("EnumSingleton初始化");
    }

    private EnumSingleton() {

    }

    public static EnumSingleton getInstance() {
        return SINGLETON;
    }

    public static void main(String[] args) {
        EnumSingleton instance = EnumSingleton.getInstance();
        System.out.println(instance);
    }
}
/**
 * EnumSingleton初始化
 * SINGLETON
 */

枚举实现的单例模式不会受到反射和序列化的影响。

6. 优点

  • 提供了对唯一实例的受控访问。因为单例类封装了它的唯一实例,所以它可以严格控制客户怎样以及何时访问它,并为设计及开发团队提供了共享的概念。
  • 由于在系统内存中只存在一个对象,因此可以节约系统资源,对于一些需要频繁创建和销毁的对象,单例模式无疑可以提高系统的性能。
  • 允许可变数目的实例。我们可以基于单例模式进行扩展,使用与单例控制相似的方法来获得指定个数的对象实例。

7. 缺点

  • 由于单例模式中没有抽象层,因此单例类的扩展有很大的困难。
  • 单例类的职责过重,在一定程度上违背了“单一职责原则”。因为单例类既充当了工厂角色,提供了工厂方法,同时又充当了产品角色,包含一些业务方法,将产品的创建和产品的本身的功能融合到一起。
  • 滥用单例将带来一些负面问题,如为了节省资源将数据库连接池对象设计为单例类,可能会导致共享连接池对象的程序过多而出现连接池溢出;现在很多面向对象语言(如Java、C#)的运行环境都提供了自动垃圾回收的技术,因此,如果实例化的对象长时间不被利用,系统会认为它是垃圾,会自动销毁并回收资源,下次利用时又将重新实例化,这将导致对象状态的丢失。

8. 适用环境

在以下情况下可以使用单例模式:

  • 系统只需要一个实例对象,如系统要求提供一个唯一的序列号生成器,或者需要考虑资源消耗太大而只允许创建一个对象。
  • 客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例。
  • 在一个系统中要求一个类只有一个实例时才应当使用单例模式。反过来,如果一个类可以有几个实例共存,就需要对单例模式进行改进,使之成为多例模式
#设计模式##单例模式#
设计模式心得 文章被收录于专栏

笔者学习设计模式的记录与心得。

全部评论

相关推荐

点赞 收藏 评论
分享
牛客网
牛客企业服务