单例-无法绕过的设计模式

2023-11-23 09:06:42 浏览数 (2)

前言

工作中我们封装Util或封装SDK都离不开单例模式,为什么要用单例模式下面是我的个人理解。

为什么使用单例模式

一些常用的工具类,由于其使用频率较高,如果每次需要使用时都新建一个对象,不仅会占用大量内存,还会导致系统负载增加,影响应用程序的性能。使用单例模式,可以在应用程序启动时就创建一个实例,直到应用程序结束时才销毁该实例,这样就能保证该工具类在整个应用程序中只有一个实例对象被使用,从而提高程序的效率和性能。

什么条件下使用单例

  • 系统中某个类的对象只需要存在一个,例如:线程池、缓存、日志对象等。 当多个对象需要共享某些信息时,单例模式可以确保这些对象都访问同一个实例,从而避免数据不一致的问题。
  • 当创建对象的开销比较大,例如对象初始化需要读取配置文件或者获取网络资源时,使用单例模式可以避免重复创建对象的开销,提高应用程序的性能和效率。
  • 某个类需要被频繁实例化,但又希望能够节省资源,避免频繁地创建和销毁对象。

单例的定义

单例模式属于创建类模式。单例的核心定义是确保某个类只有一个实例,并且自行实例化并向整个系统提供这个实例。

单例模式的优点

  • 可以避免资源的多重占用:通过单例模式,保证系统中只有一个实例,避免了多个实例占用同一资源的问题。
  • 保证了系统的灵活性和可扩展性:由于单例模式中只有一个实例对象,因此在扩展时不需要修改原来的代码,而只需增加一个实例对象即可。
  • 可以避免对资源的多重占用:对于一些需要频繁创建和销毁的对象,单例模式可以在程序初始化时直接创建,直到程序结束时才销毁,可以大大减少系统的资源占用。
  • 方便了系统的调试和维护:由于单例模式中只有一个实例对象,因此在调试时可以让开发者更容易地监控到系统的运行状况。

单例模式的缺点

  • 对于一些需要多个实例的类,单例模式不能很好地支持多例模式。
  • 单例模式具有全局状态,可能在某些情况下会对并发性能造成影响。
  • 单例模式需要考虑线程安全问题,需要通过加锁等方式来解决。
  • 单例模式可能会导致代码的耦合性较高,不利于代码的复用和维护。

单例模式的多种实现

1. 饿汉式

代码语言:javascript复制
//单例类.   
public class Singleton {
    
    private Singleton() {//构造方法为private,防止外部代码直接通过new来构造多个对象
    }

    private static final Singleton single = new Singleton();  //在类初始化时,已经自行实例化,所以是线程安全的。

    public static Singleton getInstance() {  //通过getInstance()方法获取实例对象
        return single;
    }
}  
  • 优点:

  • 实现简单:饿汉式非线程安全单例的实现比较简单,只需要在程序启动时创建单例对象即可。
  • 线程安全:由于在程序启动时就创建单例对象,因此不存在多线程访问时的线程安全问题。
  • 缺点:

  • 无法支持懒加载:在程序启动时就创建单例对象,无法支持懒加载,可能会造成资源浪费。
  • 不支持延迟加载:由于在程序启动时就创建单例对象,无法支持延迟加载,可能会造成资源浪费。
  • 不支持高并发:由于没有实现线程安全,无法支持高并发访问。

2. 懒汉式(线程不安全)

代码语言:javascript复制
//单例类
public class Singleton {
    private Singleton() {
    }

    private static Singleton single = null;

    public static Singleton getInstance() {
        if (single == null) {
            single = new Singleton();  //在第一次调用getInstance()时才实例化,实现懒加载,所以叫懒汉式
        }
        return single;
    }
} 
  • 优点:

  • 实现简单:懒汉式非线程安全单例的实现比较简单,只需要在需要时创建单例对象即可。
  • 懒加载:只有在需要时才会创建单例对象,避免了资源浪费。
  • 缺点:

  • 非线程安全:在多线程环境中不能保证单例对象的唯一性,可能会创建多个单例对象。
  • 无法支持高并发:由于没有实现线程安全,无法支持高并发访问。

3. 懒汉式(线程安全)

代码语言:javascript复制
//单例类
public class Singleton {
    private Singleton() {
    }

    private static Singleton single = null;

    public static synchronized Singleton getInstance() { //加上synchronized同步 
        if (single == null) {
            single = new Singleton();
        }
        return single;
    }
}  
  • 优点:

  • 实现简单:懒汉式线程安全单例的实现比较简单,只需要在 getInstance() 方法上加上 synchronized 关键字即可实现线程安全。
  • 懒加载:只有在需要时才会创建单例对象,避免了资源浪费。
  • 缺点:

  • 性能较低:由于使用了 synchronized 关键字,每次调用 getInstance() 方法时都需要进行加锁和解锁操作,会影响程序的性能。
  • 可伸缩性较差:由于加锁的范围较大,会导致多线程并发访问时等待时间较长,从而影响系统的可伸缩性。

4. 双重检查锁定(DCL)

代码语言:javascript复制
public class Singleton1 {
    
    //第一点:首先private是必须的,保证无法通过类名加点访问,可能是空值
    //第五点:因为以上几点原因,这里的static也是必须的
    //第十点:volatile 保证可见性,一旦修改成功,其他线程第一时间都能看到
       private static Singleton1 volatile instance = null;
       //第二点:这里的private是必须的,构造器必须私有,保证其他类无法new出对象实例
       private Singleton1() {
       }
       //第三点:要对外提供访问接口,因此获取实例的方法必须public型
       //第四点:因为其他类无法通过构造对象实例然后加点访问,只能通过类名加点访问,故必须用static
       public static Singleton1 getSingleton1Instance() {
       //第六点:第一次check
       if (null == instance) {
       //第七点:因为这里可能多个线程都会判断条件满足并进入,所以这里要加锁
           synchronized (Singleton1.class) {
             //第八点:判断条件进入的线程最终都会获得锁,因此这里进行第二次检查
             if (null == instance) {
                 instance = new Singleton1();
             }
           }
         }
         return instance;
       }
 }
  • 优点:

  • 线程安全:通过双重检查锁定机制,保证了线程安全。
  • 懒加载:在使用时才会实例化单例对象,因此实现了懒加载的效果。
  • 可以传递参数:由于单例对象的实例化在获取时才进行,因此可以通过构造函数传递参数来实现个性化的单例实例化。
  • 缺点:

  • 复杂度较高:双重检查锁定机制涉及到了多线程、volatile 变量等概念,实现起来相对复杂。
  • 可读性差:双重检查锁定机制的代码相对比较复杂,可读性较差。
  • 不适用于低版本的 Java:在 JDK 1.5 之前的版本中,由于 volatile 关键字的实现机制不同,双重检查锁定单例模式可能无法正常工作。

5. 静态内部类

代码语言:javascript复制
public class Singleton {
//第一点:构造器私有,避免外部使用new 构造该对象
private Singleton(){
}
//第四点:内部类用private修饰,表示其外部类私有,只有其外部类可以访问
//第五点:内部类用static修饰,表示该类属于外部类本身,而非外部类对象,而且外部类方法是静态的,所以内部类也必须要修饰成static
private static class SingletonHandler{
    //第六点:final 表示该引用指向的地址赋值一次之后地址不能被改变,而且在类加载的准备阶段已经将对象创建并将地址赋给
    singleton,注意,final是必须的,如果不用final,也就是说还可以new 一个对象,并将对象引用赋给singleton,这就不能保证
    唯一性
	private final static Singleton singleton = new Singleton();
}
//第二点:提供外部访问单例对象的唯一途径,所以必须是pulic类型方法
//第三点:因为外部不能构造对象,只能通过Singleton.getSingletonInstance访问该方法,所以该方法必须用static修饰
public static Singleton getSingletonInstance(){
return SingletonHandler.singleton;
}
}
  • 优点:

  • 线程安全:利用 Java 的类加载机制保证了线程安全。
  • 懒加载:静态内部类只会在使用时被加载,因此实现了懒加载的效果。
  • 防反射攻击:利用 Java 的语言特性,在静态内部类中创建单例对象,避免了反射调用私有构造方法的问题。
  • 防序列化攻击:枚举和静态内部类单例模式都可以避免序列化和反序列化的问题。
  • 缺点:

  • 无法传递参数:静态内部类单例模式无法传递参数,因此无法实现个性化的单例实例化。

6. 使用容器实现单例模式

代码语言:javascript复制
//单例管理类
public class SingletonManager {
    private static Map<String, Object> objMap = new HashMap<String, Object>();

    public static void registerService(String key, Object instance) {
        if (!objMap.containsKey(key)) {
            objMap.put(key, instance);//添加单例
        }
    }

    public static Object getService(String key) {
        return objMap.get(key);//获取单例
    }
}
  • 优点:

  • 可以支持懒加载:容器可以在需要时才创建单例实例,因此可以支持懒加载,减少了程序的启动时间和内存消耗。
  • 支持依赖注入:容器可以在创建单例实例时,同时进行依赖注入,将单例对象所依赖的对象注入进去,方便管理和维护。
  • 灵活性高:容器实现单例可以灵活地管理单例实例,方便动态添加或移除实例,以及进行动态的配置和管理。
  • 可以支持线程安全:容器可以保证单例实例的唯一性和线程安全性,可以避免在多线程环境下出现线程安全问题。
  • 缺点:

  • 依赖于容器:容器实现单例需要依赖容器,必须将单例对象的创建和管理交给容器,因此在容器不存在或者容器出现故障时,可能会导致单例模式失效。
  • 难以排查问题:容器实现单例需要依赖容器进行管理,因此一些问题可能是由容器本身引起的,而不是单例对象本身的问题,因此会增加问题排查的难度。
  • 代码量较多:容器实现单例需要编写额外的配置文件和代码,相对于其它单例实现方式,代码量较多。

7. 枚举模式单例

代码语言:javascript复制
public enum Singleton {
    INSTANCE;

    // 添加需要实现的方法
    public void doSomething() {
        // 执行某些操作
    }
}

public class TestSingleton {
    public static void main(String[] args) {
        // 获取单例实例
        Singleton singleton = Singleton.INSTANCE;

        // 调用实例方法
        singleton.doSomething();
    }
}
  • 优点:

  • 线程安全:枚举类型的实例是在类加载时创建的,且只会创建一次,因此可以保证在多线程环境下单例实例的唯一性和线程安全性。
  • 防止反射攻击:枚举类型的构造函数是私有的,不能在外部进行调用。即使使用反射机制,也无法通过调用构造函数来创建新的实例,可以有效防止反射攻击。
  • 序列化与反序列化安全:枚举类默认实现了 Serializable 接口,因此它的实例可以被序列化和反序列化。同时,由于枚举类的实例是在枚举类型中定义的,反序列化时会通过调用 valueOf() 方法来获取实例,因此可以保证序列化和反序列化的一致性和安全性。
  • 简单易用:枚举单例模式的代码量较少,实现简单,使用方便。
  • 缺点:

  • 不支持懒加载:枚举单例模式无法支持懒加载,即在需要时才进行单例实例的创建,因为枚举类型的实例是在类加载时创建的,且只会创建一次。
  • 不支持继承:由于枚举类型已经在定义时确定了实例,无法通过继承来创建新的实例,因此不支持继承。

注意事项

1. 使用反射能够破坏单例模式,所以应该慎用反射

代码语言:javascript复制
    Constructor con = Singleton.class.getDeclaredConstructor();
    con.setAccessible(true);
    // 通过反射获取实例
    Singleton singeton1 = (Singleton) con.newInstance();
    Singleton singeton2 = (Singleton) con.newInstance();
    System.out.println(singeton1==singeton2);//结果为false,singeton1和singeton2将是两个不同的实例

2. 可以通过当第二次调用构造函数时抛出异常来防止反射破坏单例,以懒汉式为例

代码语言:javascript复制
public class Singleton {
    private static boolean flag = true;
    private static Singleton single = null;

    private Singleton() {
        if (flag) {
            flag = !flag;
        } else {
            throw new RuntimeException("单例模式被破坏!");
        }
    }

    public static Singleton getInstance() {
        if (single == null) {
            single = new Singleton();
        }
        return single;
    }
}  

3. 反序列化时也会破坏单例模式,可以通过重写readResolve方法避免,以饿汉式为例:

代码语言:javascript复制
public class Singleton implements Serializable {
    private Singleton() {
    }

    private static final Singleton single = new Singleton();

    public static Singleton getInstance() {
        return single;
    }

    private Object readResolve() throws ObjectStreamException {//重写readResolve()
        return single;//直接返回单例对象
    }
} 

4. 单例对象内部资源不能过大

单例对象一旦被创建,它的生命周期会和应用程序一样长,如果该对象占用的资源过多,会导致系统的负载变高,因此需要注意控制单例对象的资源占用情况。

代码语言:javascript复制
public class MySingleton {
    private static MySingleton instance = null;

    private int[] bigArray;  // 一个大数组,占用大量内存

    private MySingleton() {
        // 初始化 bigArray
        bigArray = new int[1000000];
    }

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

    public void useBigArray() {
        // 对 bigArray 进行操作,模拟资源占用
        for (int i = 0; i < bigArray.length; i  ) {
            bigArray[i] = i;
        }
    }
}

在这个示例中,MySingleton 类是一个单例模式类,它包含一个 bigArray 数组成员变量,该数组占用了较多的内存空间。该类通过 getInstance() 方法获取单例对象,并且在构造函数中初始化了 bigArray。useBigArray() 方法模拟了对资源的占用。

在实际开发中,如果我们使用该类的实例时频繁地调用 useBigArray() 方法,可能会导致系统的负载变高,因为该方法占用了大量的内存空间。为了避免这种情况,我们可以使用一些方法来控制资源的占用,例如:

  • 采用懒加载方式延迟单例对象的创建时间,只有在需要使用单例对象时才进行初始化;
  • 优化 bigArray 数组的大小,减少其占用的内存空间; 使用缓存的方式来避免重复创建和销毁单例对象。

0 人点赞