单例模式介绍

单例模式确保一个类只有一个实例并提供全局访问点。它包括饿汉式、懒汉式、双重检查锁模式和静态内部类模式等实现方式。单例模式在日志记录、配置管理等场景中常见,但也存在违反单一职责原则、并发问题和测试困难等缺点。枚举实现是防止反射和序列化破坏单例的解决方案。
摘要由CSDN通过智能技术生成

定义

单例模式是一种创建型设计模式,它确保一个类只有一个实例,并提供了一种全局访问点以访问该实例。

关键元素

1、私有构造函数,以防止类的直接实例化

2、提供一个私有的静态成员变量,用于存储类的唯一实例

3、一个公共的静态方法,用于返回类的唯一实例。该方法负责创建实例(如果尚未创建)并返回该实例的引用

优点

1、确保一个类只有一个实例:在某些情况下,例如日志记录器或数据库连接器等,确保只有一个实例非常重要。

2、确保对象的唯一性:单例模式可以确保对象的唯一性,避免出现多个相同对象的问题。

3、减少资源消耗:由于只有一个实例,可以减少资源消耗,提高程序的效率。

缺点

1、违反单一职责原则:单例模式通常会将对象的创建和访问分离,这可能会导致代码的复杂性增加,违反单一职责原则。

2、可能会引起并发问题:如果多个线程同时访问单例对象,可能会引起并发问题。需要使用同步机制来确保线程安全。

3、难以测试:由于单例模式的对象是全局唯一的,所以在测试时可能会有一些困难。

场景

单例模式通常用于那些需要在应用程序中仅存在一个实例的情况,例如配置管理器、线程池、数据连接池等。

1、配置文件读取器:当程序需要读取配置文件时,可以使用单例模式确保只有一个读取器实例,以避免重复读取配置文件。

2、日志记录器:当程序需要记录日志时,可以使用单例模式确保只有一个日志记录器实例,以避免日志记录器被多个线程同时使用,引起并发问题。

3、数据库连接池:当程序需要访问数据库时,可以使用单例模式来管理数据库连接池,以确保只有一个连接池实例,避免资源的浪费和数据的不一致性。

4、线程池:当程序需要管理多个线程时,可以使用单例模式来管理线程池,以确保只有一个线程池实例,避免线程池的资源竞争和管理复杂度。

实现方式

饿汉式

描述

在类加载期间初始化静态实例,保证instance 实例的创建是线程安全的(实例在类加载时初始化,有JVM保证线程安全)

特点

不支持延迟加载实例(懒加载),此种方式会导致类初始化和加载比较慢,但是获取实例对象比较快

缺点

该对象足够大的话,而一直没有使用时就会造成内存的浪费

代码示例:

public class Singleton_01 {

  // 1、私有构造函数
  private Singleton_01(){
  }
  // 2、私有静态全局对象
  private static Singleton_01 instance = new Singleton_01();

  // 3、静态方法,全局访问点,提供外部获取单例对象
  public static Singleton_01 getInstance(){
         return instance;
  }
}

懒汉模式

描述

在对象初始化时,先不初始化静态实例,使用时才初始化。

特点

懒加载模式,加载类比较快,获取实例对象较慢。

问题

getInstance()方法加了同步锁(synchronzed),导致这个函数的并发度很低,在大量调用的场景下,频繁加锁、释放锁等操作,会导致性能瓶颈,这种实现方式就不可取了

代码示例:

public class Singleton_02 {

  // 1、私有构造函数
  private Singleton_02(){
  }
  // 2、私有静态全局对象
  private static Singleton_02 instance;

  // 3、静态方法,全局访问点,提供外部获取单例对象(加synchronize,保证多线程模式下的单例对象的唯一性)
  public static synchronized Singleton_02 getInstance(){
         if(instance == null){
            instance = new Singleton_02();
         }
         return instance;
  }
}

双重检查锁模式(DCL模式)

代码示例:

public class Singleton_03 {

  // 1、私有构造函数
  private Singleton_03(){
  }
  // 2、使用volatile修饰私有静态全局对象
  private volatile static Singleton_03 instance;

  // 3、静态方法,全局访问点,提供外部获取单例对象
  public static Singleton_03 getInstance(){
   
         if(instance == null){
            synchronized(Singleton_03.class){
               // 第二次判断,抢到锁之后再进程判断,时为了避免多线程下重复创建
               if(instance == null){
                  instance = new Singleton_03();
               }
            }
         }
         return instance;
  }
}

为什么要进行两次判断

1)第一次判断是在Synchronized同步代码块外进行判断,由于单例模式只会创建一个实例,并通过getInstance方法返回singleton对象,所以,第一次判断,是为了在singleton对象已经创建的情况下,避免进入同步代码块,提升效率。

2)第二次判断,抢到锁之后再进程判断,时为了避免多线程下重复创建。

在双重检查锁模式种为什么需要使用volatile关键字

1)保证变量的可见性:当一个被volatile关键字修饰的变量被一个线程修改的时候,其他线程可以立刻得到修改之后的结果。

2)屏蔽指令重排序:指令重排序是编译器和处理器为了高效对程序进行优化的手段,它只能保证程序执行的结果时正确的,但是无法保证程序的操作顺序与代码顺序一致。这在单线程中不会构成问题,但是在多线程中就会出现
问题。

静态内部类模式

代码示例:

public class Singleton04 {
   // 私有构造函数
   private Singleton04(){

   }
   // 静态内部类的特点,外部类的加载不影响内部类,同时解决了 按需加载、线程安全的问题,并实现了代码简洁
   private static class SingletonHandler {
     private static Singleton04 instance = new Singleton04();
   }
   // 提供全局访问点,供外部获取单例对象
   private static Singleton04 getInstance(){
     return SingletonHandler.instance;
   }

优点

1)在静态内部类里创建单例,在装载该内部类时才会去实例化单例。

2)线程安全:类是由JVM加载,而静态内部类JVM只会加载一次,保证只有一个单例。

3)代码简洁

枚举模式

反射技术过于强大,它可以通过 setAccessible() 来修改构造器,字段,方法的可见性。单例模式的构造方法是私有的,如果将其可见性设为 public ,那么将无法控制对象的创建。

代码示例:

public class Singleton_05 {

   private Singleton_05(){
   }

   private static class SingletonHandler{
       private static Singleton_05 instance = new Singleton_05();
   }

   public static Singleton_05 getInstance(){
          return SingletonHandler.instance;
   }
}
public class Test_Reflect {
   public static void main(String[] args) {
      try {
            //反射中,欲获取一个类或者调用某个类的方法,首先要获取到该类的Class 对象。
            Class<Singleton_05> clazz = Singleton_05.class;
            //getDeclaredXxx: 不受权限控制的获取类的成员.
            Constructor c = clazz.getDeclaredConstructor(null);
            //设置为true,就可以对类中的私有成员进行操作了
            c.setAccessible(true);
            Object instance1 = c.newInstance();
            Object instance2 = c.newInstance();
            System.out.println(instance1 == instance2);
      } catch (Exception e) {
            e.printStackTrace();
      }
   }
}

解决反射机制对单例的破坏的方案

方案一:在单例类的构造方法中 添加判断 instance != null 时,直接抛
出异常。

public class Singleton_05 {

   private Singleton_05(){

      if(SingletonHandler.instance != null){
          throw new RuntimeException("不允许非法访问!");
      }

   }

   private static class SingletonHandler{
       private static Singleton_05 instance = new Singleton_05();
   }

   public static Singleton_05 getInstance(){
          return SingletonHandler.instance;
   }
}

方案二:枚举实现(推荐方式)

public class Singleton_05{
  private Singleton_05(){
  }
  // 定义静态内部类,使用枚举实现
  public static enum SingletonEnum {
    SINGLETON;
    private Singleton_05 instance = null;
    private SingletonEnum(){
     instance  = new Singleton_05();
    }
    public Singleton_05 getInstance(){
          return instance;
    }
  }
}
public static void main(String args[]){
   Singleton_05 s1 = Singleton_05.SingletonEnum.SINGLETON.getInstance();
   Singleton_05 s2 = Singleton_05.SingletonEnum.SINGLETON.getInstance();
   System.out.println(s1==s2);
}

为什么枚举类可以阻止反射的破坏?

反射方法中不予许使用反射创建枚举对象!

为什么枚举类可以阻止序列化的破坏? 

Java规范字规定,每个枚举类型及其定义的枚举变量在JVM中都是唯一的,因此,在枚举类型的序列化和反序列化上,Java做了特殊的规定。 在序列化的时候Java仅仅是将枚举对象的name属性输到结果中,反序列化的时候则是通过java.lang.Enum的valueOf()方法来根据名字查找枚举对象。 比如说,序列化的时候只将 INSTANCE 这个名称输出,反序列化的时候再通过这个名称,查找对应的枚举类型,因此反序列化后的实例也会和之前被序列化的 对象实例相同。

 

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值