上一篇博客已经介绍了设计模式及其设计原则, 在这篇博客中笔者会介绍一下单例模式, 也是最简单的一种设计模式
单例模式 单例模式属于创建型模式. 它涉及到一个单一的类, 该类负责创建自己的对象, 同时确保只有单个对象被创建, 这个类提供了一种访问其唯一对象的方式, 可以直接访问, 不需要实例化这个类的对象
单例模式结构 单例类 . 只能创建一个实例的类访问类 . 使用单例类单例模式实现 饿汉式单例 特点 : 在类加载时就创建实例, 线程安全, 但是可能会导致资源浪费
public class Singleton { private static final Singleton instance = new Singleton (); private Singleton () {} public static Singleton getInstance () { return instance; } }
懒汉式单例 特点 : 延迟创建实例, 但是线程不安全
public class Singleton { private static Singleton instance; private Singleton () {} public static Singleton getInstance () { if (instace == null ) { instance = new Singleton (); } return instance; } }
懒汉式单例模式在多线程环境下容易导致线程不安全, 这是因为多个线程可能会同时访问 getInstance() 方法并且同时进入 if (instance == null) 代码块, 这样就会创建多个实例, 违背了单例模式的原则.
线程安全的懒汉式单例 特点 : 延迟创建实例, 使用同步方法保证线程安全, 但是会有性能开销
public class Singleton { private static Singleton instance; private Singleton () {} public static synchronized Singleton getInstance () { if (instace == null ) { instance = new Singleton (); } return instance; } }
双重检查锁 特点 : 提高性能, 减少同步开销, 线程安全
public class Singleton { private static Singleton instance; private Singleton () {} public static Singleton getInstance () { if (instance == null ) { synchronized (Singleton.class) { if (instance == null ) { instance = new Singleton (); } } } return instance; } }
双重检查锁模式可能会出现空指针问题 , 出现问题的原因是JVM在实例对象时会进行优化和指令重排序操作
为了解决空指针异常问题, 可以使用 volatile 关键字, volatile 关键字可以保证可见性和有序性
public class Singleton { private static volatile Singleton instance; private Singleton () {} public static Singleton getInstance () { if (instance == null ) { synchronized (Singleton.class) { if (instance == null ) { instance = new Singleton (); } } } return instance; } }
笔者写到这一段的时候突然想到, 如果把上面双重检查锁的代码略改一下, 改成下面这样, 是否可行?
public static Singleton getInstance () { if (instance == null ) { synchronized (Singleton.class) { instance = new Singleton (); } } return instance; } public static Singleton getInstance () { synchronized (Singleton.class) { if (instance == null ) { instance = new Singleton (); } } return instance; }
上面的两种改法, 分别是把synchronized同步块内和同步块外的判断语句 if (instance == null) 删掉之后得到的新代码.
上面这两种改法是否可行呢? 其实都不好. 对于版本1, 假设有线程1和线程2, 进行了如下操作
--------------------------------------------------------- Thread 1 Thread 2 | | | | | | | | 走到synchronized代码块处 | 拿到锁之后发生了一次线程切换 | | | | 走到synchronized代码块处, 拿不到锁, 被阻塞 | 线程切换 | | Thread 1创建了一个新实例 | Thread 1离开了synchronized代码块 | 锁被释放 | 线程切换 | | | | | | Thread 2拿到锁 | Thread 2创建了新实例 (这里违背了单例模式原则) | Thread 2离开了synchronized代码块 | Thread 2返回了创建的实例 | 线程切换 | | Thread 1返回创建的实例 | ---------------------------------------------------------
这就和线程不安全的懒汉单例模式一样了
对于版本2, 其实和使用同步代码块的懒汉单例模式也是一样的, 线程是安全的, 但是性能开销依然存在
静态内部类 特点 : 利用类加载机制实现懒加载, 线程安全
public class Singleton { private Singleton () {} private static class SingletonHelper { private static final Singleton INSTANCE = new Singleton (); } public static Singleton getInstance () { return SingletonHelper.INSTANCE; } }
枚举单例 特点 : 简单, 线程安全, 防止反序列化导致创建新的实例
public enum Singleton { INSTANCE; public void someMethod () { } }
单例模式被破坏的情况 除了枚举单例模式之外, 其他单例模式都可以被破坏. 破坏单例模式的方法有两种, 分别为 序列化 和 反射
序列化破坏单例模式 因为在序列化和反序列化过程中, 会创建一个新的实例, 即使单例类在内存中有一个唯一的实例, 通过反序列化也能创建多个实例, 这样就破坏了单例模式的初衷
假设有一个单例类如下:
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; } }
破坏单例模式的场景
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 方法, 这个方法在反序列化时会被调用, 返回当前的单例实例, 从而确保反序列化得到的始终是唯一的单例实例
改进之后的单例类
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; } protected Object readResolve () { return getInstance(); } }
再次运行 SingletonDemo , 发现 instance1 和 instance2 的哈希码是相同的, 因此它们是同一个实例, 单例模式没有被破坏.
反射破坏单例模式 因为反射允许我们访问私有构造方法, 从而构建多个对象, 这就违背了单例模式的初衷
假设有一个单例类如下:
public class Singleton { private static final Singleton instance = new Singleton (); private Singleton () {} public static Singleton getInstance () { return instance; } }
破坏单例模式的场景
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 的哈希码并不相同. 说明它们是不同的实例, 单例模式被破坏
为了防止反射破坏单例模式, 可以在构造方法中添加防御措施, 例如在构造方法中检测实例是否存在, 如果存在就抛出异常
改进之后的单例类
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, 发现反射创建实例的步骤会抛出异常, 阻止了反射破坏单例模式
最近在学设计模式, 可能会高强度更新设计模式相关的技术博客. 对设计模式感兴趣的读者可以关注我的 CSDN Channel , 掘金 Channel , 我的个人博客网站 或 网站的镜像站点