多线程下的单例模式

一.如何实现

1.懒汉模式—单线程版本

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

 

 

2.懒汉模式—多线程版本-性能低

class Singleton {
	private static Singleton instance = null;
	private Singleton() {}
	public synchronized static Singleton getInstance() {
		if (instance == null) {
			instance = new Singleton();
		} 
	return instance;
	}
}

 

 

3.懒汉模式—多线程版本—双重校验锁(重点!!!)

class Singleton {
	//1.采用volatile修饰instance变量
	private static volatile Singleton instance = null;
	//2.构造方法私有化
	private Singleton() {}
	public static Singleton getInstance() {
	//3.第一次校验:判断对象是否已经实例过,没有实例化过才进入加锁代码,否则直接返回
		if (instance == null) {
		//4.类对象加锁
			synchronized (Singleton.class) {
			//再次判断
				if (instance == null) {
					instance = new Singleton();
				}
			}
		} 
	return instance;
	}
}

 

 

二.说明

1.什么是双重校验锁

  • 第一次校验:第一个if (instance == null),为了代码提高执行效率,单例模式是只要一次创建实例即可,所以当创建了一个实例之后,再次调用getInstance()就不必要进入同步代码块,不用竞争锁,直接返回前面创建的实例即可
  • 第二次校验:第二个if (instance == null),为了防止第二次创建实例
    假设在一种情况下,当instance还未被创建时,线程A调用getInstance方法,由于第一次判断instance==null,此时线程A准备继续执行,但是由于资源被线程B抢占了,B也调用getInstance方法,这时候instance还没有被实例化,B也可以通过第一个if判断语句,然后继续往下执行同步代码块,第二个if也通过判断,这时候B线程就创建了一个实例instance,线程B完成任务,资源又回到A线程,A此时也进入同步代码块,如果没有第二个if,那么线程A此时也会创建一个instance实例,这时候就出现了创建多个实例的情况,但是如果加上第二个if判断语句,就可以完全避免由于多线程并发并行执行而导致的多次创建实例的问题

2.为什么要用双重校验锁实现单例类

一个常见情景,单例类在多线程环境中违反契约,如1.懒汉模式—单线程版本中所示的单线程版本,当这段代码在超过一个线程并行被调用的时候,会创建多个实例的问题,这时候如果将getInstance()方法设为同步(Synchronized),如2.懒汉模式—多线程版本-性能低中展示的代码,尽管这样做到了线程安全,并且解决了多实例问题,但并不高效,在任何情况调用这个方法的时候,都需要承受同步带来的性能开销,然而同步只在第一次调用的时候才被需要,也就是单例类实例创建的时候,这就促使我们使用双重校验锁模式,一种只在临界区代码加锁的方法.因为会有两次检查instance==null,一次不加锁,另一次在同步块上加锁,为了保证单例模式的正确性,还有一点是非常重要的,在声明instance变量时必须使用volatile关键字,如果没有volatile修饰符,就可能出现java中的另一个线程看到初始化了一半的instance的情况,但使用了volatile关键字变量后,就能禁止指令重排序,保证先后发生关系

3.volatile的作用

首先,我们需要知道volatile有保证可见性和禁止指令重排序的作用,但不能保证原子性,其次,对象的创建不是一步完成的,是需要分为三步操作,如以下这段代码:

instance = new Singleton();

 

这条语句可以分为三步

  • (1)为instance分配内存空间
  • (2)初始化instance
  • (3)将instance指向分配的内存空间

因为JVM具有指令重排序的特性,执行顺序有可能会变成(1)-(3)-(2),指令重排序在单线程下不会出现问题,但是如果是在多线程下,则会导致一个线程获得另一个未初始化的实例,举一个例子,比如说线程A执行了(1)和(3)指令,此时线程B调用getInstance()后发现instance不为空,因此返回instance,但是此时的instance还没有被初始化,使用volatile会禁止指令重排序,以此保证在多线程条件下程序也能正常执行

posted @ 2021-01-04 09:38  🍊爱吃橙子的丸子  阅读(207)  评论(0编辑  收藏  举报