首先我们先来看一下,普通的双重检测的单例模式:
public class Singleton{
//类加载时初始化
private static Singleton singleton;
//构造方法私有化
private Singleton(){}
//获取对象的方法
public static Singleton getSingleton(){
if(singleton == null){ //提高效率
synchronized (Singleton.class) {
if(singleton == null){ //防止多线程情况下产生两个对象
singleton = new Singleton(); // 1
return singleton; //返回一个新的对象
}
}
}
return singleton; //将之前的对象返回 // 2
}
}
这种单例模式存在什么弊端呢?
假设现在有两个线程,线程一执行到了代码1处,此时要创建对象,并赋值给singleton,这里可以分为三步:
memory = allocate(); //1:分配对象的内存空间
ctorInstance(memory); //2:初始化对象
instance = memory; //3:设置instance指向刚分配的内存地址
上面的三行伪代码中的2和3之间,可能会被重排序(在一些JIT编译器上,这种重排序是真实发生的),2和3重排序之后的执行时序如下:
memory = allocate(); //1:分配对象的内存空间
instance = memory; //3:设置instance指向刚分配的内存地址
//注意,此时对象还没有被初始化!
ctorInstance(memory); //2:初始化对象
这里首先分配了内存空间,然后让实例指向了这个地址,这个时候还没有初始化,所以这个时候指向了一个有问题的地址。
但是这时候已经指向了地址了,所以singleton已经不是空的了,如果说此时没有其他线程干扰,在创建对象到返回对象是一个单线程操作,那么是不存在问题的。 怎么会有问题呢? 假设现在有一个线程二,执行这个方法,首先判断singleton是否为空,由于此时他已经指向了一个地址,所以不是空的了,然后线程二返回了一个有问题的singleton对象,这就是存在的问题。
如下图:(截图来自并发编程网)
如何解决这个问题呢?
JDK1.5之后,可以使用volatile关键字修饰变量来解决指令重排序写入产生的问题,因为volatile关键字的一个重要作用是禁止指令重排序,即保证不会出现内存分配、返回对象引用、初始化这样的顺序,从而使得双重检测真正发挥作用。即:
public class Singleton{
//类加载时初始化 加上一个volatile防止发生指令重排序
private static volatile Singleton singleton;
//构造方法私有化
private Singleton(){}
//获取对象的方法
public static Singleton getSingleton(){
if(singleton == null){ //提高效率
synchronized (Singleton.class) {
if(singleton == null){ //防止多线程情况下产生两个对象
singleton = new Singleton(); // 1
return singleton; //返回一个新的对象
}
}
}
return singleton; //将之前的对象返回 // 2
}
}
volatile关键字不只能保证对象创建时的指令重排序,还能保证多线程下变量的重排序问题(内存模型的有序性),这是两种性质上的问题。在https://blog.csdn.net/qq_37113604/article/details/81362143中对保证变量的重排序问题进行了详细介绍。
这种写法禁止了指令重排序,但是还是存在一定的弊端,他并不能防止反射与反序列化创建两个不同的对象,又该如何解决?
先来一个测试代码:
public static void main(String[] args) {
try {
Constructor con = Singleton.class.getDeclaredConstructor();
con.setAccessible(true); //跳过java语言权限检查访问
Singleton s1 = (Singleton)con.newInstance(); //反射生成对象s1
Singleton s2 = (Singleton)con.newInstance(); //反射生成对象s2
System.out.println("s1.equals(s2): "+s1.equals(s2)); //false 两个对象
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
如何解决? 再贴一下代码与测试代码
public class Singleton{
private static boolean flag = false; //定义一个标记
//类加载时初始化
private static Singleton singleton;
//构造方法私有化
private Singleton(){
if(!flag){
flag = !flag;
}
else{
try {
throw new Exception("duplicate instance create error!" );
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
//获取对象的方法
public static Singleton getSingleton(){
if(singleton == null){ //提高效率
synchronized (Singleton.class) {
if(singleton == null){ //防止多线程情况下产生两个对象
singleton = new Singleton();
return singleton; //返回一个新的对象
}
}
}
return singleton; //将之前的对象返回
}
public static void main(String[] args) {
try {
Constructor con = Singleton.class.getDeclaredConstructor();
con.setAccessible(true); //跳过java语言权限检查访问
Singleton s1 = (Singleton)con.newInstance(); //反射生成对象s1
Singleton s2 = (Singleton)con.newInstance(); //反射生成对象s2
System.out.println("s1.equals(s2): "+s1.equals(s2));
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
执行结果:第二次反射生成对象时,抛出了异常,这样就有效的防止了反射带来的安全隐患。
如果这个单例不需要用对象流传输,那么到这里就已经不再有隐患了,如果实现了Serializable接口,需要进行序列化,那么这里还存在最后一个问题,对象流在序列化与反序列化的时候,会不会产生两个不同的对象呢?
贴一下代码:
public class Singleton implements Serializable{
private static final long serialVersionUID = 1L;
private static boolean flag = false; //定义一个标记
//类加载时初始化
private static Singleton singleton;
//构造方法私有化
private Singleton(){
if(!flag){
flag = !flag;
}
else{
try {
throw new Exception("duplicate instance create error!" );
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
//获取对象的方法
public static Singleton getSingleton(){
if(singleton == null){ //提高效率
synchronized (Singleton.class) {
if(singleton == null){ //防止多线程情况下产生两个对象
singleton = new Singleton();
return singleton; //返回一个新的对象
}
}
}
return singleton; //将之前的对象返回
}
public static void main(String[] args) throws Exception {
Singleton s1 = Singleton.getSingleton();
FileOutputStream fos = new FileOutputStream(new File("D:/bzy/1.txt"));
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(s1); //将对象序列化存入1.txt
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("D:/bzy/1.txt")));
Singleton s2 = (Singleton) ois.readObject(); //反序列化生成对象
System.out.println("s2.equals(s1): "+s2.equals(s1)); //false不是同一对象
}
}
打印结果:
解决办法:加入一个readResolve方法,当从I/O流中读取对象时,readResolve()方法都会被调用到,然后将当前存在的对象返回,实际上就是用readResolve()中返回的对象(当前对象)直接替换在反序列化过程中创建的对象。
再来贴一下代码:
public class Singleton implements Serializable{
private static final long serialVersionUID = 1L;
private static boolean flag = false; //定义一个标记
//类加载时初始化
private static Singleton singleton;
//构造方法私有化
private Singleton(){
if(!flag){
flag = !flag;
}
else{
try {
throw new Exception("duplicate instance create error!" );
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
//获取对象的方法
public static Singleton getSingleton(){
if(singleton == null){ //提高效率
synchronized (Singleton.class) {
if(singleton == null){ //防止多线程情况下产生两个对象
singleton = new Singleton();
return singleton; //返回一个新的对象
}
}
}
return singleton; //将之前的对象返回
}
private Object readResolve() throws ObjectStreamException {
return singleton;//将当前对象返回,如果当前对象为空,那么即使反序列化了一个对象还是返回null
}
public static void main(String[] args) throws Exception {
Singleton s1 = Singleton.getSingleton();
FileOutputStream fos = new FileOutputStream(new File("D:/bzy/1.txt"));
ObjectOutputStream oos = new ObjectOutputStream(fos);
oos.writeObject(s1); //将对象序列化存入1.txt
ObjectInputStream ois = new ObjectInputStream(new FileInputStream(new File("D:/bzy/1.txt")));
Singleton s2 = (Singleton) ois.readObject(); //反序列化生成对象
System.out.println("s2.equals(s1): "+s2.equals(s1)); //false不是同一对象
}
}
打印结果:
到这里双重检测的单例模式,可以说已经没有安全隐患了。下面的几种单例模式,就不再进行一一深入分析了
饿汉模式的对象创建在类加载时创建,此时还不存在线程调用的情况,所以不会出现上面的隐患一,但是隐患二和隐患三是存在的,单例模式的最终版:
public class Singleton implements Serializable {
private static final long serialVersionUID = 1L;
private static boolean flag = false;
private static final Singleton singleton = new Singleton();
private Singleton() {
if (!flag) {
flag = !flag;
} else {
try {
throw new Exception("duplicate instance create error!");
} catch (Exception e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
public static Singleton getInstance() {
return singleton;
}
private Object readResolve() throws ObjectStreamException {
return singleton;
}
}
由于静态内部类的特性,只有在其被第一次引用的时候才会被加载,然后加载静态类中的静态变量,这些操作在线程调度之前,所以可以保证其线程安全性,不存在隐患一。但是同样存在隐患二与隐患三,最终版:
public class Instance implements Serializable{
private static final long serialVersionUID = 1L;
private static boolean flag = false;
private Instance() {
if (!flag) {
flag = !flag;
} else {
try {
throw new Exception("duplicate instance create error!");
} catch (Exception e) {
e.printStackTrace();
}
}
}
private Object readResolve() throws ObjectStreamException {
return getInstance();
}
private static class InstanceHolder {
public static Instance instance = new Instance();
}
public static Instance getInstance() {
return InstanceHolder.instance ; //调用时InstanceHolder类被初始化
}
}
枚举的单例模式写起来十分简单,由于枚举在编译后,INSTANCE的创建是依靠类加载的所以不存在隐患一。
经过测试枚举模式创建的单例可以有效的防止隐患二,因为在getConstructor时就会报错,为什么会报错吗?通过反编译枚举类可以发现,类的修饰为abstract,所以没法实例化,反射也无能为力。
也可以有效防止隐患三,Java规范中规定,每一个枚举类型极其定义的枚举变量在JVM中都是唯一的,因此在枚举类型的序列化和反序列化上,Java做了特殊的规定。在序列化的时候Java仅仅是将枚举对象的name属性输出到结果中,反序列化的时候则是通过 java.lang.Enum 的 valueOf() 方法来根据名字查找枚举对象。
也就是说,以下面枚举为例,序列化的时候只将 INSTANCE 这个名称输出,反序列化的时候再通过这个名称,查找对于的枚举类型,因此反序列化后的实例也会和之前被序列化的对象实例相同。
enum SingletonDemo {
INSTANCE
}
最后送大家一段苹果的广告语,致疯狂的人
他们特立独行。他们桀骜不驯。他们惹是生非。他们格格不入。他们用与众不同的眼光看待事物。他们不喜欢墨守成规。他们也不愿安于现状。你可以认同他们,反对他们,颂扬或是诋毁他们。但唯独不能漠视他们。因为他们改变了寻常事物。他们推动人类向前迈进。或许他们是别人眼里的疯子,但他们却是我们眼中的天才。因为只有那些疯狂到以为自己能够改变世界的人,才能真正改变世界。