普通双重检测单例模式真的是单例吗?

研究完了volatile关键字,再来深入了解下单例模式,以及单例模式中存在的各种隐患,以及解决方案,最后给出几种不存在隐患的单例模式

一. 双重检测单例模式

首先我们先来看一下,普通的双重检测的单例模式:

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
	}
}

这种单例模式存在什么弊端呢?

隐患一:(对象创建时重排序问题,解决方法:volatile关键字)

假设现在有两个线程,线程一执行到了代码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中对保证变量的重排序问题进行了详细介绍。

扫描二维码关注公众号,回复: 3406765 查看本文章

隐患二:(反射创建对象带来的安全隐患,解决办法:设置标记变量)

这种写法禁止了指令重排序,但是还是存在一定的弊端,他并不能防止反射与反序列化创建两个不同的对象,又该如何解决?

先来一个测试代码:

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();
		} 
	}
} 

执行结果:第二次反射生成对象时,抛出了异常,这样就有效的防止了反射带来的安全隐患。

隐患三:(序列化与反序列化带来的安全隐患,解决办法:定义readResolve方法)

如果这个单例不需要用对象流传输,那么到这里就已经不再有隐患了,如果实现了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()中返回的对象(当前对象)直接替换在反序列化过程中创建的对象。

再来贴一下代码:

双重检测最终版单例模式:(除去main方法)

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
}

最后送大家一段苹果的广告语,致疯狂的人 

      他们特立独行。他们桀骜不驯。他们惹是生非。他们格格不入。他们用与众不同的眼光看待事物。他们不喜欢墨守成规。他们也不愿安于现状。你可以认同他们,反对他们,颂扬或是诋毁他们。但唯独不能漠视他们。因为他们改变了寻常事物。他们推动人类向前迈进。或许他们是别人眼里的疯子,但他们却是我们眼中的天才。因为只有那些疯狂到以为自己能够改变世界的人,才能真正改变世界。

猜你喜欢

转载自blog.csdn.net/qq_37113604/article/details/81435840