设计模式-单例模式(Singleton)
设计模式-单例模式(Singleton)
概要
记忆关键字: 唯一实例
定义:保证一个类仅有一个实例,并提供一个访问它的全局访问点
分析:唯一实例的受控访问,可以严格地控制客户怎么样访问以及何时访问
单例模式结构图如下:
一、解决什么问题?
单例模式可以用来管理一些共享资源,比如数据库连接池,线程池;解决资源冲突问题,比如日志打印。节省内存空间,比如配置信息类。
二、单例模式实现
1. 实现方式一:双重锁检测
1 public class Singleton1 { 2 private volatile static Singleton1 singleton1 = null; 3 4 private Singleton1() { 5 6 } 7 8 /** 9 * 双重锁检测-实现单例模式 10 * 11 * @return 执行结果 12 */ 13 public static Singleton1 getInstance() { 14 //先判断对象是否已经实例过,没有实例化过才进入加锁代码 15 if (singleton1 == null) { 16 //类对象加锁 17 synchronized (Singleton1.class) { 18 if (singleton1 == null) { 19 singleton1 = new Singleton1(); 20 } 21 } 22 } 23 24 return singleton1; 25 } 26 }
说明:线程安全、属于懒加载、不能防止发射构建
需要注意的是singleton1变量需要添加volatile关键字,防止指令重排。这样就基本上解决了性能问题、内存浪费问题。
2. 实现方式二:静态内部类
1 public class Singleton2 { 2 3 4 private Singleton2() { 5 } 6 7 private static class LazyHolder { 8 private static final Singleton2 INSTANCE = new Singleton2(); 9 } 10 11 /** 12 * 静态内部类-实现单例模式 13 * @return 执行结果 14 */ 15 public static Singleton2 getInstance() { 16 return LazyHolder.INSTANCE; 17 18 } 19 }
说明:线程安全、属于懒加载、不能防止发射构建
静态内部类 LazyHolder 只有在第一次被引用时(即调用 getInstance() 方法)才会被加载和初始化。这种方式确保了 Singleton2 实例在第一次使用时才被创建,从而实现了懒加载。
这种实现方式被认为是最优雅的单例模式实现之一,推荐在实际开发中使用。
3. 实现方式三:枚举类
1 public enum SingletonEnum { 2 /** 3 * 枚举类-实现单例模式 4 */ 5 INSTANCE 6 }
说明:线程安全、属于饿汉式(在类加载时立即创建实例)、能够防止发射构建
三. 单例模式的作用
1)控制对象的数量
有的类其内部实现复杂,如果频繁创建销毁对象,是比较消耗服务器资源的
2)全局访问
单例模式的特点是单例类自己持有这个单例对象,并且提供一个静态方法可在全局获取到这个单例对象。
说明:如果没有单例模式的情况下,我们一般是在代码A处创建这个对象,在代码B处如果也要使用这个对象,就需要将这个对象进行参数传递。为了避免传来传去,我们可能会写个Holder类,把这个对象放在Holder的成员变量中。而单例模式的这个优点是,我们可以避免这样的困扰,直接从单例类中获取。
四、补充
1. volatile关键字不但可以防止指令重排,也可以保证线程访问的变量值是主内存中的最新值
2. 使用枚举实现的单例模式,不但可以防止利用反射强行构建单例对象,而且可以在枚举类对象被反序列化的时候,保证反序列的返回结果是同一对象。
3. 对于其他方式实现的单例模式,如果既想要做到可序列化,又想要反序列化为同一对象,则必须实现readResolve方法。代码示例如下:
1 import java.io.ObjectStreamException; 2 import java.io.Serializable; 3 4 public class Singleton implements Serializable { 5 private static final long serialVersionUID = 1L; 6 7 // 静态内部类实现单例模式 8 private static class SingletonHolder { 9 private static final Singleton INSTANCE = new Singleton(); 10 } 11 12 // 私有构造函数,防止外部实例化 13 private Singleton() { 14 if (SingletonHolder.INSTANCE != null) { 15 throw new IllegalStateException("Instance already created"); 16 } 17 } 18 19 public static Singleton getInstance() { 20 return SingletonHolder.INSTANCE; 21 } 22 23 // 实现 readResolve 方法,确保反序列化返回同一个实例 24 private Object readResolve() throws ObjectStreamException { 25 return getInstance(); 26 } 27 28 public void showMessage() { 29 System.out.println("Hello, I am a singleton!"); 30 } 31 }
关于readResolve方法:
1)readResolve 方法并不是 Serializable 接口本身定义的,而是序列化机制的一部分,虽然 Serializable 接口是一个标记接口(没有任何方法),但 Java 的序列化机制提供了一个名为 readResolve 的钩子方法,这个方法可以在你的类中自定义,以控制反序列化的行为。这是一种设计上的约定,旨在为开发者提供控制反序列化对象实例的机会。
2)当一个类实现了 Serializable 接口,并且在该类中定义了一个 readResolve 方法,Java的反序列化机制会在反序列化时自动调用这个方法。这个方法允许你在对象被反序列化后,用一个新的对象(通常是单例)来替换反序列化得到的对象。
3)当一个对象被反序列化时,JVM 会检查这个对象所属的类是否定义了 readResolve 方法。
4)如果找到了 readResolve 方法,JVM 将调用它,返回的对象将取代原本反序列化得到的对象。
五、应用场景
1. Spring Bean的默认作用域
Spring容器中的bean默认是单例的,这意味着无论你在容器中请求多少次该bean,容器都会返回同一个实例。
2. 使用@Bean注解定义单例bean
即使你使用@Bean注解来定义bean,默认情况下也是单例的。
3. 单例bean中的依赖注入
由于Spring默认将bean定义为单例,因此在单例bean中注入其他单例bean时,可以确保这些依赖也是单例的。
4. Application Context作为单例
Spring的`ApplicationContext`本身也是一个单例,它在整个应用程序中仅有一个实例,用于管理和创建所有的bean。
5. 数据库连接池和线程池都采用单例模式
参考链接:https://juejin.cn/post/7163943247705243655