单例模式是一种设计模式,指在内存中只会创建且仅创建一次对象的设计模式。
在程序中多次使用同一个对象且作用相同时,为了防止频繁地创建对象使得内存飙升,单例模式可以让程序仅在内存中创建一个对象,让所有需要调用的地方都共享这一单例对象,它确保一个类只有一个实例,并且提供一个全局访问点。
单例模式有两种类型:
-
懒汉式:在真正需要使用对象时才去创建该单例类对象 懒汉式创建对象的方法是在程序使用对象前,先判断该对象是否已经实例化(判空),若已实例化直接返回该类对象。否则则先执行实例化操作。
-
饿汉式:在类加载时已经创建好该单例对象,等待被程序使用 饿汉式在类加载时已经创建好该对象,在程序调用时直接返回该单例对象即可,即我们在编码时就已经指明了要马上创建这个对象,不需要等到被调用时再去创建。
1. 单例模式的示例
懒汉式(线程不安全)
如果两个线程同时判断 singleton 为空,那么它们都会去实例化一个 Singleton 对象,这就变成双例了,所以不安全。
public class Singleton { private static Singleton instance; private Singleton() {} public static Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
懒汉式(线程安全)
惰性初始化
synchronized
关键字来确保在创建实例时只有一个线程可以执行getInstance()
方法。
当一个线程开始执行这个方法时,它会获得一个锁,这个锁会阻止其他线程进入这个方法,直到第一个线程完成执行并释放锁。
public class Singleton { private static Singleton instance; private Singleton() {} public static synchronized Singleton getInstance() { if (instance == null) { instance = new Singleton(); } return instance; } }
饿汉式
public class Singleton { // 使用final关键字,一旦被实例化,就不能被改变 private static final Singleton instance = new Singleton(); private Singleton() {} public static Singleton getInstance() { return instance; } }
双重检查锁定(Double-Checked Locking)——>懒汉写法
因为需要两次判空,且对类对象加锁,该懒汉式写法也被称为:Double Check(双重校验) + Lock(加锁)
public class Singleton { // 使用 volatile 防止指令重排 private static volatile Singleton instance; private Singleton() {} public static Singleton getInstance() { // 线程A和线程B同时看到instance = null if (instance == null) { synchronized (Singleton.class) { // 其中一个线程进入该分支,另外一个线程则不会进入该分支 if (instance == null) { instance = new Singleton(); } } } // 如果不为null,则直接返回singleton return instance; } }
静态内部类——>懒汉写法
“双重检查锁定”(double-checked locking)的一种变种,利用了静态内部类的特性,避免了与锁相关的开销和复杂性
public class Singleton { private Singleton() {} // 私有的,并且只有在调用getInstance()方法时才会被加载和初始化,这确保了INSTANCE字段的初始化是线程安全的 private static class SingletonHolder { private static final Singleton INSTANCE = new Singleton(); } public static Singleton getInstance() { return SingletonHolder.INSTANCE; } }
枚举方式(Java特有的单例实现)
public enum Singleton { INSTANCE; public void someMethod() { // 实现方法 System.out.println("这是枚举类型的单例模式!"); } } // 测试 public enum Singleton { INSTANCE; Singleton() { System.out.println("枚举创建对象了"); } public static void main(String[] args) { /* test(); */ } public void test() { Singleton t1 = Singleton.INSTANCE; Singleton t2 = Singleton.INSTANCE; System.out.print("t1和t2的地址是否相同:" + t1 == t2); } } // 枚举创建对象了 // t1和t2的地址是否相同:true
(1)Enum 类内部使用 Enum 类型判定防止通过反射创建多个对象
(2)Enum 类通过写出(读入)对象类型和枚举名字将对象序列化(反序列化),每个枚举类型和枚举名字都是唯一的,通过 valueOf() 方法匹配枚举名找到内存中的唯一的对象实例,防止通过反序列化构造多个对象
(3)枚举类不需要关注线程安全、破坏单例和性能问题,因为其创建对象的时机与饿汉式单例有异曲同工之妙。
2. 单例的应用场景
单例模式通常用于以下场景:
-
需要全局访问点的类,如配置管理器。
-
控制资源使用,如数据库连接池。
-
需要一个全局状态的类,如日志记录器。
懒汉的使用场景:
-
适合于实例化代价较大,或者实例可能不会被使用,或者希望延迟实例化的场景。
饿汉的使用场景:
-
适合于那些实例必须存在,或者实例化代价不大,且实例不会变化的情况。
性能考虑:
-
饿汉式:由于实例化是在类加载时完成的,所以获取实例的速度非常快,但缺点是不管是否需要,实例都会被创建。
-
懒汉式:第一次获取实例时需要进行同步操作(如果考虑线程安全的话),这可能会稍微影响性能,但之后获取实例的速度会很快。
资源利用:
-
饿汉式:不管是否使用,类加载时就完成实例化,这可能会导致资源的浪费,如果这个单例很少被使用的话。
-
懒汉式:只有在第一次调用
getInstance()
方法时才会创建实例,这可以延迟对象的创建,从而减少资源的浪费。
3. 单例的好处
-
控制实例数量:确保整个应用中只有一个实例。
-
资源节约:避免了创建多个实例时的资源浪费。
-
数据共享:提供了一个共享的数据存储点。
-
线程安全:通过适当的实现,可以确保线程安全。
4. 单例的缺点
-
全局状态:可能导致代码难以测试和维护。
-
扩展性差:单例模式使得扩展为多例变得困难。
-
依赖问题:单例类可能成为其他类的依赖,违反了依赖倒置原则。
-
线程安全问题:实现不当可能导致线程安全问题。
5. 单例的注意事项
-
线程安全:确保在多线程环境下单例的实现是线程安全的。
-
延迟加载:考虑是否需要延迟加载实例。
-
序列化问题:如果单例类需要被序列化,需要提供一个
readResolve
方法来防止创建新的实例。(可以创建出多个具有相同状态但实际上是不同实例的对象)-
你首先通过
Singleton.getInstance()
获取了单例模式的唯一实例,并将其序列化到一个文件中。 -
然后你从文件中反序列化出这个对象,得到一个新的
Singleton
实例。 -
最后你比较这两个实例是否是同一个对象(即比较它们的内存地址),结果是
false
,因为它们是两个不同的对象实例。 -
private Object readResolve() { // 返回单例实例,而不是新创建的实例 return getInstance(); } 即使你通过反序列化创建了一个
Singleton
对象,你也只会得到与通过getInstance()
方法相同的单例实例。
-
-
反射攻击:防止通过反射破坏单例。
Java的反射机制允许在运行时检查和修改类、接口、字段和方法的可见性、修饰符和泛型信息,甚至可以创建和调用构造器。
-
单例类的设计:单例类应该是封闭的,不允许继承。
子类可能会覆盖父类的单例实现,创建多个实例.
子类可能会添加新的状态或行为
-
测试困难:单例模式可能会使得单元测试变得困难,因为它们通常依赖于全局状态。
7. 总结
(1)单例模式常见的写法有两种:懒汉式、饿汉式
(2)懒汉式:在需要用到对象时才实例化对象,正确的实现方式是:Double Check + Lock,解决了并发安全和性能低下问题
(3)饿汉式:在类加载时已经创建好该单例对象,在获取单例对象时直接返回对象即可,不会存在并发安全和性能问题。
(4)在开发中如果对内存要求非常高,那么使用懒汉式写法,可以在特定时候才创建该对象;
(5)如果对内存要求不高使用饿汉式写法,因为简单不易出错,且没有任何并发安全和性能问题
(6)为了防止多线程环境下,因为指令重排序导致变量报 NPE,需要在单例对象上添加 volatile 关键字防止指令重排序
(7)最优雅的实现方式是使用枚举,其代码精简,没有线程安全问题,且 Enum 类内部防止反射和反序列化时破坏单例。