@c102zkl
2018-08-29T12:29:59.000000Z
字数 3346
阅读 546
设计模式
为什么这样写呢?我们来解释几个关键点:
1.要想让一个类只能构建一个对象,自然不能让它随便去做new操作,因此Signleton的构造方法是私有的。
2.instance是Singleton类的静态成员,也是我们的单例对象。它的初始值可以写成Null,也可以写成new Singleton()。至于其中的区别后来会做解释。
3.getInstance是获取单例对象的方法。
如果单例初始值是null,还未构建,则构建单例对象并返回。这个写法属于单例模式当中的懒汉模式。
如果单例对象一开始就被new Singleton()主动构建,则不再需要判空操作,这种写法属于饿汉模式。
但第一版不是线程安全的
假设Singleton类刚刚被初始化,instance对象还是空,这时候两个线程同时访问getInstance方法:
因为Instance是空,所以两个线程同时通过了条件判断,开始执行new操作:
这样一来,显然instance被构建了两次。让我们对代码做一下修改:
//单例模式
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;
}
为什么这样写呢?我们来解释几个关键点:
1.为了防止new Singleton被执行多次,因此在new操作之前加上Synchronized 同步锁,锁住整个类(注意,这里不能使用对象锁)。
2.进入Synchronized 临界区以后,还要再做一次判空。因为当两个线程同时访问的时候,线程A构建完对象,线程B也已经通过了最初的判空验证,不做第二次判空的话,线程B还是会再次构建instance对象。
像这样两次判空的机制叫做双重检测机制。
第二版还是有个漏铜,就是指令重排序
假设这样的场景,当两个线程一先一后访问getInstance方法的时候,当A线程正在构建对象,B线程刚刚进入方法:
这种情况表面看似没什么问题,要么Instance还没被线程A构建,线程B执行 if(instance == null)的时候得到true;要么Instance已经被线程A构建完成,线程B执行 if(instance == null)的时候得到false。
真的如此吗?答案是否定的。这里涉及到了JVM编译器的指令重排。
指令重排是什么意思呢?比如java中简单的一句 instance = new Singleton,会被编译器编译成如下JVM指令:
memory =allocate(); //1:分配对象的内存空间
ctorInstance(memory); //2:初始化对象
instance =memory; //3:设置instance指向刚分配的内存地址
但是这些指令顺序并非一成不变,有可能会经过JVM和CPU的优化,指令重排成下面的顺序:
memory =allocate(); //1:分配对象的内存空间
instance =memory; //3:设置instance指向刚分配的内存地址
ctorInstance(memory); //2:初始化对象
当线程A执行完1,3,时,instance对象还未完成初始化,但已经不再指向null。此时如果线程B抢占到CPU资源,执行 if(instance == null)的结果会是false,从而返回一个没有初始化完成的instance对象。如下图所示:
如何避免这一情况呢?我们需要在instance对象前面增加一个修饰符volatile。
第三版
//单例模式
private static volatile Singleton instance = null; // 单例对象
private Singleton(){} // 私有构造函数
//静态工厂方法
public static Singleton getInstance(){
if (instance == null) { //双重检测机制
synchronized (Singleton.class) { //同步锁
if (instance == null) { //双重检测机制
instance = new Singleton();
}
}
}
return instance;
}
经过volatile的修饰,当线程A执行instance = new Singleton的时候,JVM执行顺序是什么样?始终保证是下面的顺序:
memory =allocate(); //1:分配对象的内存空间
ctorInstance(memory); //2:初始化对象
instance =memory; //3:设置instance指向刚分配的内存地址
如此在线程B看来,instance对象的引用要么指向null,要么指向一个初始化完毕的Instance,而不会出现某个中间态,保证了安全。
package design_mode.singleton20180725;
public class Singleton2 {
private static class SingletonFactory{
private static final Singleton2 instance = new Singleton2();
}
private Singleton2(){}
public static Singleton2 getInstance(){
return SingletonFactory.instance;
}
}
这里有几个需要注意的点:
1.从外部无法访问静态内部类SingletonFactory
,只有当调用SingletonFactory.getInstance
方法的时候,才能得到单例对象instance
。
2.instance
对象初始化的时机并不是在单例类Singleton2被加载的时候,而是在调用getInstance方法,使得静态内部类SingletonFactory
被加载的时候。因此这种实现方式是利用classloader的加载机制
来实现懒加载,并保证构建单例的线程安全。
静态内部类的实现方式,还是前面的单例模式实现都一样,无法防止利用反射来重复构造对象。
//利用反射打破单例
//获得构造器
Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
//设置为可访问
constructor.setAccessible(true);
//构造两个不同的对象
Singleton singleton1 = constructor.newInstance();
Singleton singleton2 = constructor.newInstance();
//验证是否是不同对象
System.out.println(singleton1.equals(singleton2));
public enum Singleton3 {
INSTANCE;
}
JVM会阻止反射获取枚举类的私有构造方法,使用枚举类实现单例模式不仅能够防止反射构造对象,而且可以保证线程安全。有一个唯一缺点就是,它并非使用懒加载
,其单例对象实在枚举类被加载的时候进行初始化的。
2.使用枚举实现的单例模式,不但可以防止利用反射强行构建单例对象,而且可以在枚举类对象被反序列化的时候,保证反序列的返回结果是同一对象。
对于其他方式实现的单例模式,如果既想要做到可序列化,又想要反序列化为同一对象,则必须实现readResolve方法。