设计模式-单例模式(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

posted @ 2020-05-28 14:07  欢乐豆123  阅读(192)  评论(0编辑  收藏  举报