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关键字创建对象并不是一个原子性操作,它会经历这三个指令
- 分配内存空间
- 执行构造方法,初始化对象
- 把对象指向这块空间
如果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. 适用环境
在以下情况下可以使用单例模式:
- 系统只需要一个实例对象,如系统要求提供一个唯一的序列号生成器,或者需要考虑资源消耗太大而只允许创建一个对象。
- 客户调用类的单个实例只允许使用一个公共访问点,除了该公共访问点,不能通过其他途径访问该实例。
- 在一个系统中要求一个类只有一个实例时才应当使用单例模式。反过来,如果一个类可以有几个实例共存,就需要对单例模式进行改进,使之成为多例模式
笔者学习设计模式的记录与心得。