前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >单例-无法绕过的设计模式

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

作者头像
Coder昊白
发布2023-11-23 09:06:42
1700
发布2023-11-23 09:06:42
举报

前言

工作中我们封装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 数组的大小,减少其占用的内存空间; 使用缓存的方式来避免重复创建和销毁单例对象。
本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2023-11-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 前言
  • 为什么使用单例模式
  • 什么条件下使用单例
  • 单例的定义
  • 单例模式的优点
  • 单例模式的缺点
  • 单例模式的多种实现
    • 1. 饿汉式
      • 2. 懒汉式(线程不安全)
        • 3. 懒汉式(线程安全)
          • 4. 双重检查锁定(DCL)
            • 5. 静态内部类
              • 6. 使用容器实现单例模式
                • 7. 枚举模式单例
                • 注意事项
                  • 1. 使用反射能够破坏单例模式,所以应该慎用反射
                    • 2. 可以通过当第二次调用构造函数时抛出异常来防止反射破坏单例,以懒汉式为例
                      • 3. 反序列化时也会破坏单例模式,可以通过重写readResolve方法避免,以饿汉式为例:
                        • 4. 单例对象内部资源不能过大
                        相关产品与服务
                        容器服务
                        腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
                        领券
                        问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档