简谈设计模式之单例模式

单例模式

单例模式属于创建型模式. 它涉及到一个单一的类, 该类负责创建自己的对象, 同时确保只有单个对象被创建, 这个类提供了一种访问其唯一对象的方式, 可以直接访问, 不需要实例化这个类的对象

单例模式结构

  • 单例类. 只能创建一个实例的类
  • 访问类. 使用单例类

单例模式实现

  1. 饿汉式单例

特点: 在类加载时就创建实例, 线程安全, 但是可能会导致资源浪费

 

csharp

代码解读

复制代码

 public class Singleton {      // 在类加载时就创建实例instance      private static final Singleton instance = new Singleton();            // 私有的构造函数, 避免从外部构造新实例      private Singleton() {}            // 提供一个全局访问的接口, 可以获取已经创建好的单个实例instance      public static Singleton getInstance() {          return instance;     }  }

  1. 懒汉式单例

特点: 延迟创建实例, 但是线程不安全

整理了这份面试笔记包括了:Java面试、Spring、JVM、MyBatis、Redis、MySQL、并发编程、微服务、Linux、Springboot、SpringCloud、MQ、Kafka 面试专题

需要全套面试笔记【点击此处】即可免费获取

csharp

代码解读

复制代码

 public class Singleton {      // 单个的实例      private static Singleton instance;            // 私有的构造函数, 避免从外部构造新实例      private Singleton() {}            // 提供一个全局访问的接口, 可以获取已经创建好的单个实例instance      public static Singleton getInstance() {          // 假如这时instance还没有被创建, 那么就创建一个新的实例instance          if (instace == null) {              instance = new Singleton();         }          return instance;     }  }

懒汉式单例模式在多线程环境下容易导致线程不安全, 这是因为多个线程可能会同时访问 getInstance() 方法并且同时进入 if (instance == null) 代码块, 这样就会创建多个实例, 违背了单例模式的原则.

  1. 线程安全的懒汉式单例

特点: 延迟创建实例, 使用同步方法保证线程安全, 但是会有性能开销

 

csharp

代码解读

复制代码

 public class Singleton {      // 单个的实例      private static Singleton instance;            // 私有的构造函数, 避免从外部构造新实例      private Singleton() {}            // 提供一个全局访问的接口, 可以获取已经创建好的单个实例instance, 使用同步方法保证线程安全      public static synchronized Singleton getInstance() {          // 假如这时instance还没有被创建, 那么就创建一个新的实例instance          if (instace == null) {              instance = new Singleton();         }          return instance;     }  }

  1. 双重检查锁

特点: 提高性能, 减少同步开销, 线程安全

 

csharp

代码解读

复制代码

 public class Singleton {      // 单个的实例      private static Singleton instance;            // 私有的构造函数, 避免从外部构造新实例      private Singleton() {}            // 提供一个全局访问的接口, 可以获取已经创建好的单个实例instance      public static Singleton getInstance() {          // 第一次判断实例是否为null, 如果不为null就直接返回实例, 不进入抢锁阶段          if (instance == null) {              synchronized (Singleton.class) {                  // 抢到锁了再判断一次是否为null             if (instance == null) {                      instance = new Singleton();                 }             }         }          return instance;     }  }

双重检查锁模式可能会出现空指针问题, 出现问题的原因是JVM在实例对象时会进行优化和指令重排序操作

为了解决空指针异常问题, 可以使用 volatile 关键字, volatile 关键字可以保证可见性和有序性

 

csharp

代码解读

复制代码

 public class Singleton {      // 单个的实例, 使用volatile关键字保证其可见性和有序性      private static volatile Singleton instance;            // 私有的构造函数, 避免从外部构造新实例      private Singleton() {}            // 提供一个全局访问的接口, 可以获取已经创建好的单个实例instance      public static Singleton getInstance() {          // 第一次判断实例是否为null, 如果不为null就直接返回实例, 不进入抢锁阶段          if (instance == null) {              synchronized (Singleton.class) {                  // 抢到锁了再判断一次是否为null             if (instance == null) {                      instance = new Singleton();                 }             }         }          return instance;     }  }

笔者写到这一段的时候突然想到, 如果把上面双重检查锁的代码略改一下, 改成下面这样, 是否可行?

 

csharp

代码解读

复制代码

 // Double-Checked Locking version 1  public static Singleton getInstance() {      if (instance == null) {          synchronized (Singleton.class) {              instance = new Singleton();         }     }      return instance;  }  //======================================  // Double-Checked Locking version 2  public static Singleton getInstance() {      synchronized (Singleton.class) {          if (instance == null) {              instance = new Singleton();         }     }      return instance;  }

上面的两种改法, 分别是把synchronized同步块内和同步块外的判断语句 if (instance == null) 删掉之后得到的新代码.

上面这两种改法是否可行呢? 其实都不好. 对于版本1, 假设有线程1和线程2, 进行了如下操作

 

arduino

代码解读

复制代码

 ---------------------------------------------------------         Thread 1                   Thread 2             |                           |             |                           |             |                           |             |                           |  走到synchronized代码块处                 |  拿到锁之后发生了一次线程切换               |             |                           |             |                       走到synchronized代码块处, 拿不到锁, 被阻塞             |                       线程切换             |                           |  Thread 1创建了一个新实例                 |  Thread 1离开了synchronized代码块         |  锁被释放                                 |  线程切换                                 |             |                           |             |                           |             |                       Thread 2拿到锁             |                       Thread 2创建了新实例 (这里违背了单例模式原则)             |                       Thread 2离开了synchronized代码块             |                       Thread 2返回了创建的实例             |                       线程切换             |                           |  Thread 1返回创建的实例                   |  ---------------------------------------------------------

这就和线程不安全的懒汉单例模式一样了

对于版本2, 其实和使用同步代码块的懒汉单例模式也是一样的, 线程是安全的, 但是性能开销依然存在

  1. 静态内部类

特点: 利用类加载机制实现懒加载, 线程安全

 

csharp

代码解读

复制代码

 public class Singleton {      // 私有的构造函数, 避免从外部构造新实例      private Singleton() {}            // 静态内部类, 延迟加载      private static class SingletonHelper {          private static final Singleton INSTANCE = new Singleton();     }            // 提供一个全局访问的接口, 可以获取已经创建好的单个实例instance      public static Singleton getInstance() {          return SingletonHelper.INSTANCE;     }  }

  1. 枚举单例

特点: 简单, 线程安全, 防止反序列化导致创建新的实例

 

csharp

代码解读

复制代码

public enum Singleton { INSTANCE; // 其他方法 public void someMethod() { // do something } }

单例模式被破坏的情况

除了枚举单例模式之外, 其他单例模式都可以被破坏. 破坏单例模式的方法有两种, 分别为 序列化 和 反射

  1. 序列化破坏单例模式

因为在序列化和反序列化过程中, 会创建一个新的实例, 即使单例类在内存中有一个唯一的实例, 通过反序列化也能创建多个实例, 这样就破坏了单例模式的初衷

假设有一个单例类如下:

 

java

代码解读

复制代码

import java.io.Serializable public class Singleton implements Serializable { private static final long serialVersionUID = 1L; private static final Singleton instance = new instance(); private Singleton(); public Singleton getInstance() { return instance; } // other methods... }

破坏单例模式的场景

 

java

代码解读

复制代码

import java.io.*; public class SingletonDemo { public static void main(String[] args) { try { Singleton instance1 = Singleton.getInstance(); // 序列化 ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("singleton.ser")); out.writeObject(instance1); out.close(); // 反序列化 ObjectInputStream in = new ObjectInputStream(new FileInputStream("singleton.ser")); Singleton instance2 = (Singleton) in.readObject(); in.close; System.out.println("Instance 1 hash code: " + instance1.hashCode()); System.out.println("Instance 2 hash code: " + instance2.hashCode()); } catch (Exception e) { e.printStackTrace(); } } }

运行 SingletonDemo, 发现 instance1 和 instance2 的哈希码并不相同, 说明它们是不同的实例, 这就破坏了单例模式

为了防止序列化破坏单例模式, 可以在单例类中定义 readResolve 方法, 这个方法在反序列化时会被调用, 返回当前的单例实例, 从而确保反序列化得到的始终是唯一的单例实例

改进之后的单例类

 

java

代码解读

复制代码

import java.io.Serializable public class Singleton implements Serializable { private static final long serialVersionUID = 1L; private static final Singleton instance = new instance(); private Singleton(); public Singleton getInstance() { return instance; } // 添加readResolve方法 protected Object readResolve() { return getInstance(); } // other methods... }

再次运行 SingletonDemo , 发现 instance1 和 instance2 的哈希码是相同的, 因此它们是同一个实例, 单例模式没有被破坏.

  1. 反射破坏单例模式

因为反射允许我们访问私有构造方法, 从而构建多个对象, 这就违背了单例模式的初衷

假设有一个单例类如下:

 

csharp

代码解读

复制代码

public class Singleton { private static final Singleton instance = new Singleton(); private Singleton() {} public static Singleton getInstance() { return instance; } }

破坏单例模式的场景

 

java

代码解读

复制代码

import java.lang.reflect.Constructor; public class SingletonDemo { public static void main(String[] args) { try { Singleton instance1 = Singleton.getInstance(); // 通过反射创建新的实例 Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor(); constructor.setAccessible(true); Singleton instance2 = constructor.newInstance(); // 检查两个实例是否相同 System.out.println("Instance 1 hash code: " + instance1.hashCode()); System.out.println("Instance 2 hash code: " + instance2.hashCode()); } catch (Exception e) { e.printStackTrace(); } } }

运行 SingletonDemo, 发现 instance1 和 instance2 的哈希码并不相同. 说明它们是不同的实例, 单例模式被破坏

为了防止反射破坏单例模式, 可以在构造方法中添加防御措施, 例如在构造方法中检测实例是否存在, 如果存在就抛出异常

改进之后的单例类

 

csharp

代码解读

复制代码

public class Singleton { private static final Singleton instance = new Singleton(); private Singleton() { // 防止反射创建新的实例 if (instance != null) { throw new RuntimeException("Use getInstance() method to get the single instance of this class."); } } public static Singleton getInstance() { return instance; } }

再次运行 SingletonDemo, 发现反射创建实例的步骤会抛出异常, 阻止了反射破坏单例模式

  • 13
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值