重学设计模式(一)单例模式
单例模式
单例模式是最简单的设计模式之一,他提供了一种创建对象的最佳方式。 这种模式涉及到一个单一的类,该类负责创建自己类的对象,同时确保只会有一个对象会被创建。这个类提供了一种访问其唯一的对象的方式,使用者不需要实例化该类的对象。
单例模式的实现方式比较多,主要体现在是否支持延迟加载、是否线程安全。当然有些场景这些都不需要考虑,直接用静态类或者方法进行处理就能满足需求。
饿汉式
其实静态类变量也算饿汉式。
public class Singleton {
private static Singleton instance;
static {
instance = new Singleton();
}
//私有构造方法
private Singleton(){
}
//外界获取实例的方法
public static Singleton getInstance(){
return instance;
}
}
这种方式在类加载的时候就会被实例化,那么这种方式导致的问题就像你下载个游戏软件,可能你游戏地图还没有打开呢,但是程序已经将这些地图全部实例化。到你手机上最明显体验就一开游戏内存满了,手机卡了,需要换了。
懒汉式
-
线程不安全
public class Singleton { private static Singleton instance = null; //私有构造方法 private Singleton(){ } //外界获取实例的方法 public static Singleton getInstance(){ if (instance == null){ instance = new Singleton(); } return instance; } }
目前此种方式的单例确实满足了懒加载,但是如果有多个访问者同时去获取对象实例你可以想象成一堆人在抢厕所,就会造成多个同样的实例并存,从而没有达到单例的要求。
-
线程安全
- 方法一:
public class Singleton { private static Singleton instance = null; //私有构造方法 private Singleton(){ } //外界获取实例的方法 public static synchronized Singleton getInstance(){ if (instance == null){ instance = new Singleton(); } return instance; } }
-
方法二:双重检验法
第一种方法存在性能被大幅降低的问题,获取实例的方法在绝大多数情况下都是读操作,本来就是线程安全的,只有在第一次调用的时候才会有写操作。因此,只需要在第一次操n作时加上同步锁即可,如何判断是否是第一次呢,那就是双重检验法。
public class Singleton { private static Singleton instance = null; //私有构造方法 private Singleton(){ } //外界获取实例的方法 public static Singleton getInstance(){ if (instance == null){ synchronized (Singleton.class){ if (instance == null){ instance = new Singleton(); } } } return instance; } }
这种方式看似很完美,即解决了线程安全问题,还保证了性能,但它还存在问题。在并发情况下,还会出现空指针的问题。
在编译阶段编译器会对源代码进行指令重排优化,在JVM中运行的时候实例化对象也是分好几步操作的。简单来说就是:
- 在栈中申请空间用于存放引用
- 在堆中申请空间用于存放对象实例
- 初始化对象
- 将堆中的地址赋值给引用
这种方式使用双重检查锁,多线程环境下执行getInstance()时先判断单例对象是否已经初始化,如果已经初始化,就直接返回单例对象,如果未初始化,就在同步代码块中先进行初始化,然后返回,效率很高。
但是这种方式是一个错误的优化,问题的根源出在位置2
sInstance =new Singleton();这句话创建了一个对象,他可以分解成为如下3行代码:
memory = allocate(); // 1.分配对象的内存空间 ctorInstance(memory); // 2.初始化对象 sInstance = memory; // 3.设置sInstance指向刚分配的内存地址
上述伪代码中的2和3之间可能会发生重排序,重排序后的执行顺序如下
memory = allocate(); // 1.分配对象的内存空间 sInstance = memory; // 2.设置sInstance指向刚分配的内存地址,此时对象还没有被初始化 ctorInstance(memory); // 3.初始化对象
因为这种重排序并不影响Java规范中的规范:intra-thread sematics允许那些在单线程内不会改变单线程程序执行结果的重排序。但是多线程并发时可能会出现以下情况,线程B访问到的是一个还未初始化的对象。
这时候使用
volatile
关键字就可以保证这种有序性,这时候就不会发生指令重排。
静态内部类
这种方式单例对象由静态的内部类创建,由于JVM在记载外部类的过程中,是不会加载内部类的只有内部类被使用时才会被加载。
public class Singleton {
//私有构造方法
private Singleton(){
}
//外界获取实例的方法
public static Singleton getInstance(){
return SingletonHolder.instance;
}
private static class SingletonHolder{
public static Singleton instance = new Singleton();
}
}
使用类的静态内部类实现的单例模式,既保证了线程安全有保证了懒加载,同时不会因为加锁的方式耗费性能。这主要是因为JVM虚拟机可以保证多线程并发访问的正确性,也就是一个类的构造方法在多线程环境下可以被正确的加载.
破坏单例模式
上述实现方法无法保证这种单例模式不会被破坏,使用Java提供的反射机制和反序列化都可以破坏。
解决方案:
-
反序列化:
在单例类中添加
readResolve()
方法,该方法会在反序列化时被反射调用,如果定义了这个方法,就返回这个方法的返回值,如果没有才根据序列字节码。public class Singleton { //私有构造方法 private Singleton(){ } //外界获取实例的方法 public static Singleton getInstance(){ return SingletonHolder.instance; } private static class SingletonHolder{ public static Singleton instance = new Singleton(); } private Object readResolve(){ return SingletonHolder.instance; } }
-
反射:
由于反射机制可以直接调用一个类中的所有方法,即使这个方法时私有的。既然没法阻止调用私有的构造方法,那就在构造方法中添加一些逻辑,如果已经有实例了那就直接抛出异常,停止这个方法。
public class Singleton { //私有构造方法 private static boolean flag = false; private Singleton(){ if (flag){ throw new RuntimeException("不能创建多个对象"); } flag = true; } //外界获取实例的方法 public static Singleton getInstance(){ return SingletonHolder.instance; } private static class SingletonHolder{ public static Singleton instance = new Singleton(); } private Object readResolve(){ return SingletonHolder.instance; } }