多线程单例模式的实现

public class MyObject {

	private static MyObject myObject;

	private MyObject() {
	}

	
	synchronized public static MyObject getInstance() {
		try {
			if (myObject != null) {
			} else {
				// 模拟在创建对象之前的准备性工作
				Thread.sleep(3000);
				myObject = new MyObject();
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		return myObject;
	}

}

但是这种方法的运行效率十分低下,尝试使用同步代码块

public class MyObject {

	private static MyObject myObject;

	private MyObject() {
	}

	public static MyObject getInstance() {
		try {
			if (myObject != null) {
			} else {
				Thread.sleep(3000);
				synchronized (MyObject.class) {
					myObject = new MyObject();
				}
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		return myObject;
	}

}

这种写法看似提升了效率,但是是不正确的。因为在同步代码块里没有判断对象是否为null,也就是说多个线程在判断myObject不为null时,同时来到了代码块,接下来它们会出个创建出对象。

public class MyObject {

	private static MyObject myObject;
	public int method(){
		return 1;
	}
	private MyObject() {
	}

	public static MyObject getInstance() {
		try {
			if (myObject != null) {
			} else {
				Thread.sleep(3000);
				synchronized (MyObject.class) {
					if (myObject == null) {
						myObject = new MyObject();
					}
				}
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		return myObject;
	}

}

在同步代码块里加了一行判断代码,这似乎看起已经非常正确了。

如果第一次检查instance不为null,那么就不需要执行下面的加锁和初始化操作。因此,可以大幅降低synchronized带来的性能开销。上面代码表面上看起来,似乎两全其美。
·多个线程试图在同一时间创建对象时,会通过加锁来保证只有一个线程能创建对象。
·在对象创建好之后,执行getInstance()方法将不需要获取锁,直接返回已创建好的对象。
双重检查锁定看起来似乎很完美,但这是一个错误的优化!

根据《java并发编程的艺术》里面解释:

前面的双重检查锁定示例代码的第7行(myObject=new MyObject();)创建了一个对象。这一行代码可以分解为如下的3行伪代码。

memory = allocate(); // 1:分配对象的内存空间
ctorInstance(memory); // 2:初始化对象
instance = memory; // 3:设置instance指向刚分配的内存地址


上面3行伪代码中的2和3之间,可能会被重排序(在一些JIT编译器上,这种重排序是真实发生的,详情见参考文献1的“Out-of-order writes”部分)。2和3之间重排序之后的执行时序如下。

memory = allocate(); // 1:分配对象的内存空间
instance = memory; // 3:设置instance指向刚分配的内存地址
// 注意,此时对象还没有被初始化!
ctorInstance(memory); // 2:初始化对象

 基于volatile的解决方案

对于前面的基于双重检查锁定来实现延迟初始化的方案(指DoubleCheckedLocking示例代码),只需要做一点小的修改(把myObject声明为volatile型),就可以实现线程安全的延迟初始化。请看下面的示例代码。

public class MyObject {

	private static volatile MyObject myObject;
	public int method(){
		return 1;
	}
	private MyObject() {
	}

	public static MyObject getInstance() {
		try {
			if (myObject != null) {
			} else {
				Thread.sleep(3000);
				synchronized (MyObject.class) {
					if (myObject == null) {
						myObject = new MyObject();
					}
				}
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		return myObject;
	}

}

当声明对象的引用为volatile后,上面3行伪代码中的2和3之间的重排序,在多线程环境中将会被禁止。

使用静态内部类实现单例模式

public class MyObject {

	private static class MyObjectHandler {
		private static MyObject myObject = new MyObject();
	}

	private MyObject() {
	}

	public static MyObject getInstance() {
		return MyObjectHandler.myObject;
	}

}

使用static代码块实现单例模式

public class MyObject {

	private static MyObject instance = null;

	private MyObject() {
	}

	static {
		instance = new MyObject();
	}

	public static MyObject getInstance() {
		return instance;
	}

}

使用静态内部类实现单例模式和使用static代码块实现单例模式的原理是

根据《深入理解java虚拟机》:

类初始化阶段是类加载过程的最后一步,前面的类加载过程中,除了在加载阶段用户应用程序可以通过自定义类加载器参与之外,其余动作完全由虚拟机主导和控制。到了初始化阶段,才真正开始执行类中定义的Java程序代码(或者说是字节码)。在准备阶段,变量已经赋过一次系统要求的初始值,而在初始化阶段,则根据程序员通过程序制定的主观计划去初始化类变量和其他资源,或者可以从另外一个角度来表达:初始化阶段是执行类构造器<clinit>()方法的过程。

<clinit>()方法是由编译器自动收集类中的所有类变量的赋值动作和静态语句块(static{}块)中的语句合并产生的,编译器收集的顺序是由语句在源文件中出现的顺序所决定的,静态语句块中只能访问到定义在静态语句块之前的变量,定义在它之后的变量,在前面的静态语句块可以赋值,但是不能访问。

虚拟机会保证一个类的<clinit>()方法在多线程环境中被正确地加锁、同步,如果多个线程同时去初始化一个类,那么只会有一个线程去执行这个类的<clinit>()方法,其他线程都需要阻塞等待,直到活动线程执行<clinit>()方法完毕。

猜你喜欢

转载自blog.csdn.net/ploter/article/details/81606020