前言
工作中我们封装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 数组的大小,减少其占用的内存空间; 使用缓存的方式来避免重复创建和销毁单例对象。