单例设计模式-Double Check

 

单例设计模式主要是为了保证只创建一个对象,其余时候需要复用的话就直接引用那个对象即可。简单来说,就是在整个应用中保证只有一个类的实例存在。

我们常用的单例模式有  饿汉式单例 和 饱汉式单例

 

饿汉式单例设计模式

package com.imodule.dataImport.study;
/**
 * 饿汉式单例设计模式
 * 1.定义一个静态私有成员变量 并初始值为当前类的实例化对象
 * 2.私有化构造方法(防止被外部通过 new 构造方法创建对象)
 * 3.定义public的静态方法返回当前类的实例化对象
 * @author S0111
 *
 */
public class SingleTon1 {

	private static final SingleTon1 instance = new SingleTon1();
	    
	private SingleTon1() {}
	    
	public static SingleTon1 getInstance() {
	     return instance;
	}
}

优点:实现起来简单,没有多线程同步问题。

  缺点:当类Singletont被加载的时候,会初始化static的instance,静态变量被创建并分配内存空间,从这以后,这个static的instance对象便一直占着这段内存(即便你还没有用到这个实例),当类被卸载时,静态变量被摧毁,并释放所占有的内存,因此在某些特定条件下会耗费内存。

 

饱汉式单例设计模式-初版(适合单线程访问)

package com.imodule.dataImport.study;
/**
 * 饱汉-单例设计模式
 * 1.定义一个静态的私有成员变量,初始值为null
 * 2.定义私有的构造方法(防止被外部通过 new 构造方法创建对象)
 * 3.定义一个静态的public获取对象的方法,里卖先判断对象是否为空
 * 		是: 通过构造方法创建对象再返回
 * 		否: 直接返回对象
 * @author S0111
 *
 */
public class SingleTon {

	private static SingleTon instance = null;
	private SingleTon(){}
	
	public static SingleTon getInstance(){
		if(instance == null){
			instance = new SingleTon();
		}
		return instance;
	}
}

优点:实现起来比较简单,当类SingletonTest被加载的时候,静态变量static的instance未被创建并分配内存空间,当getInstance方法第一次被调用时,初始化instance变量,并分配内存,因此在某些特定条件下会节约了内存。

缺点:在多线程环境中,这种实现方法会有问题,不能完全保证单例的状态。

 

在多线程访问的情况下以上单例是可能会失效的,因为创建对象分为三步

eg: instance = new SingleTon();

1.为单例对象在堆上分配内存 //mem = allocate();  

2.创建SingleTon的实例对象  // ctorSingleton(instance);

3.将实例对象 instance 指向内存空间 (这一步instance才为非null) // instance = mem; 

Java编译为字节码后,JVM 的即时编译器中存在指令重排序的优化 ,有可能进行指令重排序 ,最终的执行顺序可能是 1-2-3 也可能是 1-3-2(2与3互换顺序)

1.为单例对象在堆上分配内存 //mem = allocate();  

3.将实例对象 instance 指向内存空间 (这一步instance才为非null) // instance = mem; 

2.创建SingleTon的实例对象  // ctorSingleton(instance);

如果指令的执行顺序如上 1-3-2,则在 3 执行完毕、2 未执行之前,被线程二抢占了,这时 instance 已经是非 null 了(但却没有初始化),所以线程二会直接返回 instance(此时的instance为null),然后被调用使用,从而会报错。

这里的关键在于 —— 线程thread1对instance的写操作没有完成,线程thread2就执行了读操作

 

饱汉式单例 多线程访问-方案一(在获取实例的方法上加锁 synchronized)

public class SingleTon {

	private static SingleTon instance = null;
	private SingleTon(){}
	
	//直接加锁 但是这样子效率有点低
	public static synchronized SingleTon getInstance(){
		if(instance == null){
			instance = new SingleTon();
		}
		return instance;
	}
}

不推荐使用,效率太低了啦

 

饱汉式单例 多线程访问-方案二 也就是本文的重点啦 (Double-check)

1.使用volatile关键字修饰 instance,防止指令重排序

2.加上同步代码块  保证线程安全(比同步方法高效,因为同步方法每次获取对象都会执行,同步代码块只在第一次获取对象时会执行,后期会直接返回对象信息)

public class SingleTon {

	private static volatile SingleTon instance = null;// volatile 关键字修饰变量 防止指令重排序
	private SingleTon(){}
	
	public static  SingleTon getInstance(){
		if(instance == null){
			//同步代码块 只有在第一次获取对象的时候会执行到 ,第二次及以后访问时 instance变量均非null故不会往下执行了 直接返回啦
			synchronized(SingleTon.class){
				if(instance == null){
					instance = new SingleTon();
				}
			}
		}
		return instance;
	}
}

 推荐使用!!!

 

volatile关键字的两层语义

  一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

  1)保证了不同线程对这个变量进行操作时的可见性,即一个线程修改了某个变量的值,这新值对其他线程来说是立即可见的。

  2)禁止进行指令重排序。