单例模式的学习

单例模式是一种设计模式,指在内存中只会创建且仅创建一次对象的设计模式。

在程序中多次使用同一个对象且作用相同时,为了防止频繁地创建对象使得内存飙升,单例模式可以让程序仅在内存中创建一个对象,让所有需要调用的地方都共享这一单例对象,它确保一个类只有一个实例,并且提供一个全局访问点。

单例模式有两种类型:

  • 懒汉式:在真正需要使用对象时才去创建该单例类对象 懒汉式创建对象的方法是在程序使用对象前,先判断该对象是否已经实例化(判空),若已实例化直接返回该类对象。否则则先执行实例化操作。

  • 饿汉式:在类加载时已经创建好该单例对象,等待被程序使用 饿汉式在类加载时已经创建好该对象,在程序调用时直接返回该单例对象即可,即我们在编码时就已经指明了要马上创建这个对象,不需要等到被调用时再去创建

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方法来防止创建新的实例。(可以创建出多个具有相同状态但实际上是不同实例的对象)

    1. 你首先通过 Singleton.getInstance() 获取了单例模式的唯一实例,并将其序列化到一个文件中。

    2. 然后你从文件中反序列化出这个对象,得到一个新的 Singleton 实例。

    3. 最后你比较这两个实例是否是同一个对象(即比较它们的内存地址),结果是 false,因为它们是两个不同的对象实例。

    4. private Object readResolve() { // 返回单例实例,而不是新创建的实例 return getInstance(); } 即使你通过反序列化创建了一个 Singleton 对象,你也只会得到与通过 getInstance() 方法相同的单例实例。

  • 反射攻击:防止通过反射破坏单例。

    Java的反射机制允许在运行时检查和修改类、接口、字段和方法的可见性、修饰符和泛型信息,甚至可以创建和调用构造器。

  • 单例类的设计:单例类应该是封闭的,不允许继承。

    子类可能会覆盖父类的单例实现,创建多个实例.

    子类可能会添加新的状态或行为

  • 测试困难:单例模式可能会使得单元测试变得困难,因为它们通常依赖于全局状态。

7. 总结

(1)单例模式常见的写法有两种:懒汉式、饿汉式

(2)懒汉式:在需要用到对象时才实例化对象,正确的实现方式是:Double Check + Lock,解决了并发安全和性能低下问题

(3)饿汉式:在类加载时已经创建好该单例对象,在获取单例对象时直接返回对象即可,不会存在并发安全和性能问题。

(4)在开发中如果对内存要求非常高,那么使用懒汉式写法,可以在特定时候才创建该对象;

(5)如果对内存要求不高使用饿汉式写法,因为简单不易出错,且没有任何并发安全和性能问题

(6)为了防止多线程环境下,因为指令重排序导致变量报 NPE,需要在单例对象上添加 volatile 关键字防止指令重排序

(7)最优雅的实现方式是使用枚举,其代码精简,没有线程安全问题,且 Enum 类内部防止反射和反序列化时破坏单例

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值