独一无二的对象——单例模式
前言
在我看来这是最简单的设计模式之一
介绍
独一无二的对象。
为什么需要单例模式?
有一些共享资源对象我们只需要一个,比如说线程池,缓存,注册表,日志对象等。通常创建和销毁这些对象会消耗更多的资源,比如I/O和数据库连接等,如果频繁创建和销毁会造成不必要的性能浪费。
如何实现单例模式?
要实现单例模式,主要要考虑三个因素
- 是不是否线程安全
- 是否延迟实例化
- 能不能通过反射破坏
抛开这三个因素我们实现一个单例模式。
-
要实现单例,就必然不能让外界实例化(通过new 可以创建多个对象), 所以我们要做的就是将构造器私有化,然后提供一个静态方法获取实例
public class Singleton { private Singleton(){} // 构造器私有化 private static Singleton instance = new Singleton(); public static Singleton getInstance() { return instance; } }
这样我们就能保证这是一个独一无二的对象了(至少看起来是这样!);
-
如果考虑延迟实例化(懒汉模式):
懒汉模式考虑到某些对象创建和销毁的开销比较大而应用启动后却没有调用过而导致的资源浪费
public class Singleton { private Singleton(){} // 构造器私有化 private static Singleton instance = null; public static Singleton getInstance() { if(instance == null){ instance = new Singleton(); } return instance; } }
-
是否线程安全?
熟悉多线程的同学应该就能看的出来,这段代码存在的线程安全问题。多线程环境下可能会有多个线程同时判断instance == null, 而导致Singleton被实例化多次。结果可能是不同线程拿到了不同的实例,导致的诡异的bug。《Head First 设计模式》 中单例模式因为线程不安全而导致的巧克力锅炉异常的例子很有意思,有兴趣的同学👉 https://book.douban.com/subject/2243615/
如何解决?
方式1:Synchronized
public class Singleton { private Singleton(){} // 构造器私有化 private static Singleton instance = null; public static synchronized Singleton getInstance() { if(instance == null){ instance = new Singleton(); } return instance; } }
这样线程确实安全了,但是我们只想要在构建对象的时候进行同步,现在导致的结果是我每次获取对象都要进行同步操作...... 这无疑是捡了芝麻,丢了西瓜。
方式2:在编译器构建对象,在运行时候调用
// 这不就是 1 步的方式吗 ? 害, 但是不是懒加载哦 public class Singleton { private Singleton(){} // 构造器私有化 private static Singleton instance = new Singleton(); public static Singleton getInstance() { return instance; } }
方式3: 因为是创建对象才需要做同步,获取对象不需要所以可以使用双重检查加锁,
public class Singleton { private static Singleton uniqueInstance; private Singleton(){} public static Singleton getInstance() { // 第一次检查对象是否创建,如果已经创建就直接放回,不用走同步代码 if(uniqueInstance == null) { synchronized (Singleton.class){ // 第二次检查是因为当前线程可能不是第一个拿到锁的线程,如果不进行检查,就会被创建多次 if(uniqueInstance == null){ uniqueInstance = new Singleton(); } } } return uniqueInstance; } }
uniqueInstance = new Singleton(); 没有遵循happen-before原则。因为在指令层面这不是一个原子操作,实例化分为了三步 1,分配内存 2, 初始化对象 3,对象指向内存。 而虚拟机为了效率可能会进行指令重排序,可能导致的结果就是 先执行第一步再执行第三步,最后才执行第二步。 如果当前线程是第一个拿到锁的线程,并且进行了1 -> 3 ->2 这样的指令重排序的话,在第3步指向了内存地址,却没有初始化时,另一个线程就会判断成 uniqueInstance != null; 那么另一个线程拿到的就可能是一个还未初始胡的对象。那么如何解决
解决: volatile: 确保volatile写之前的操作不会被编译重排序到volatile写之后,读volatile变量时候,可以确保读之后的操作不会被编译重排序到volatile读之前。
public class Singleton { private volatile static Singleton uniqueInstance; private Singleton(){} public static Singleton getInstance() { // 第一次检查对象是否创建,如果已经创建就直接放回,不用走同步代码 if(uniqueInstance == null) { synchronized (Singleton.class){ // 第二次检查是因为当前线程可能不是第一个拿到锁的线程,如果不进行检查,就会被创建多次 if(uniqueInstance == null){ uniqueInstance = new Singleton(); } } } return uniqueInstance; } }
方式4: 终极解决方案 线程安全+懒加载+代码更简洁+效率比较高: 静态内部类
public class Singleton { private static class SingletonHolder { private static final Singleton instance = new Singleton(); } private Singleton() {} public static final Singleton getInstance(){ return SingletonHolder.instance; } }
-
能否被反射破坏,通过反射是可以拿到该类的构造方法的.