单例模式与多线程

一、前言

       如何使单例模式遇到多线程是安全的、正确的?

       我们在学习设计模式的时候知道单例模式有懒汉式和饿汉式之分。简单来说,饿汉式就是在使用类的时候已经将对象创建完毕,懒汉式就是在真正调用的时候进行实例化操作。

二、饿汉式+多线程

单例:

public class MyObject {
    //饿汉模式
	
	private static MyObject myObject=new MyObject();
	private MyObject(){
		
	}
	public static MyObject getInstance(){
		return myObject;
	}
}

 自定义线程:

public class MyThread extends Thread {
	@Override
	public void run(){
		System.out.println(MyObject.getInstance().hashCode());
	}
}

main方法:

public class Run {
 
	public static void main(String[] args) {
		MyThread t1=new MyThread();
		MyThread t2=new MyThread();
		MyThread t3=new MyThread();
		t1.start();
		t2.start();
		t3.start();
 
	}
 
}

结果:

 hashCode是同一个值,说明对象是同一个。也就是说饿汉式单例模式在多线程环境下是线程安全的。

三、懒汉式+多线程

方案一:

单例:

public class MyObject {
	private static MyObject myObject;
	/*私有构造函数避免被实例化*/
	private MyObject(){
		
	}
	public static MyObject getInstance(){
		try {
			if (myObject==null) {
				//模拟在创建对象之前做的一些准备性工作
				Thread.sleep(3000);
				myObject=new MyObject();
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		return myObject;
	}
 
}

自定义线程:

public class MyThread extends Thread {
  @Override
  public void run(){
	  System.out.println(MyObject.getInstance().hashCode());
  }
}

main方法:

public class Run {
 
	public static void main(String[] args) {
		MyThread t1=new MyThread();
		MyThread t2=new MyThread();
		MyThread t3=new MyThread();
		t1.start();
		t2.start();
		t3.start();
 
	}
 
}

结果:

  3种hashCode,说明创建出了3个对象,并不是单例的。懒汉模式在多线程环境下是“非线程安全”。这是为何?

因为创建实例对象的那部分代码没有加synchronized或Lock。三个线程都进入了创建实例对象的代码段getInstance。

方案二:synchronized同步方法

      既然多个线程可以同时进入getInstance()方法,那么只需要对getInstance()方法声明synchronized关键字即可。在MyObject的getInstance()方法前加synchronized关键字。最终打印的三个hashcode是一样一样的。实现了多线程环境下,懒汉模式的正确性、安全性。但是此种方法的运行效率非常低下,因为是同步的,一个线程释放锁之后,下一个线程继续执行。
方案三:synchronized同步代码块

 同步方法是对方法整体加锁,效率不高,我们可以通过减少锁的粒度,也就是使用synchronized同步代码块。如下面代码所示:

public class MyObject {
	private static MyObject myObject;
	/*私有构造函数避免被实例化*/
	private MyObject(){
		
	}
	/*synchronized*/
	public  static MyObject getInstance(){
		try {
			synchronized (MyObject.class) {
				if (myObject==null) {
					//模拟在创建对象之前做的一些准备性工作
					Thread.sleep(3000);
					myObject=new MyObject();
				}
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		return myObject;
	}
 
} 

     这样做能保证最终运行结果正确,但getInstance方法中的全部代码都是同步的了,这样做会降低运行效率,和对getInstance方法加synchronized的效率几乎一样。

方案四:重要代码同步代码块

public class MyObject {
	private static MyObject myObject;
	/*私有构造函数避免被实例化*/
	private MyObject(){
		
	}
	/*synchronized*/
	public  static MyObject getInstance(){
		try {
			
			if (myObject==null) {
				//模拟在创建对象之前做的一些准备性工作
				Thread.sleep(3000);
				synchronized (MyObject.class) {
				myObject=new MyObject();
				}
			}
			
		} catch (Exception e) {
			e.printStackTrace();
		}
		return myObject;
	}
 
}

结果:

 这种做法在多线程环境下还是无法解决得到同一个实例对象的结果。

方案五:双重锁定

package singleton_3;
 
public class MyObject {
	private static MyObject myObject;
	/*私有构造函数避免被实例化*/
	private MyObject(){
		
	}
	//使用双重锁定(Double-Check Locking)解决问题,既保证了不需要同步代码的异步执行性,
	//又保证了单例的效果
	public static MyObject getInstance(){
		try {
			if (myObject==null) {
				//模拟在创建对象之前做一些准备性的工作
				Thread.sleep(3000);
				synchronized(MyObject.class){
					if (myObject==null) {
						myObject=new MyObject();
					}
				}
				
			}
		} catch (Exception e) {
			e.printStackTrace();
		}
		return myObject;
	}
 
}

      使用双重锁定功能,成功地解决了在多线程环境下“懒汉模式”的“非线程安全”问题。
      那么为什么外面已经判断myObject实例是否存在,为什么在lock里面还需要做一次myObject实例是否存在的判断呢?

      如果myObject已经存在,则直接返回,这没有问题。当Instance为null,并且同时有3个线程调用GetInstance()方法时,它们都可以通过第一重myObject==null的判断,然后由于lock机制,这三个线程只有一个进入,另外2个在外排队等候,必须第一个线程走完同步代码块之后,第二个线程才进入同步代码块,此时判断instance==null,为false,直接返回myObject实例。就不会再创建新的实例啦。第二个监测myObject==null一定要在同步代码块中。
方案六:

     方案五表面上来看,在执行该代码时,先判断instance对象是否为空,为空时再进行初始化对象。即使是在多线程环境下,因为使用了synchronized锁进行代码同步,该方法也仅仅创建一个实例对象。但是,从根本上来说,这样写还是存在一定问题的。  问题源头:

创建对象:1.创建对象时限分配内存空间-----》2.初始化对象-----》3.设置对象指向内存空间-----》4.初次访问对象;

2和3可能存在重排序问题,由於单线程中遵守intra-thread semantics,从而能保证即使2和3交换顺序后其最终结果不变。但是当在多线程情况下,线程B将看到一个还没有被初始化的对象,此时将会出现问题。

 解决方案:

1、不允许②和③进行重排序

2、允许②和③进行重排序,但排序之后,不允许其他线程看到。

基于volatile的解决方案

对前面的双重锁实现的延迟初始化方案进行如下修改:  

public class MyObject {
	private volatile static MyObject myObject;

	/* 私有构造函数避免被实例化 */
	private MyObject() {

	}

	/* synchronized */
	public static MyObject getInstance() {
		try {

			if (myObject == null) {
				// 模拟在创建对象之前做的一些准备性工作
				Thread.sleep(3000);
				synchronized (MyObject.class) {
					if (myObject == null) {
						myObject = new MyObject(); // 用volatile修饰,不会再出现重排序
					}
				}
			}

		} catch (Exception e) {
			e.printStackTrace();
		}
		return myObject;
	}

}

使用volatile修饰instance之后,之前的②和③之间的重排序将在多线程环境下被禁止,从而保证了线程安全执行。
   注意:这个解决方案需要JDK5或更高版本(因为从JDK5开始使用新的JSR-133内存模型规范,这个规范增强了volatile的语义)

基于类初始化的解决方案

   JVM在类的初始化阶段(即在Class被加载后,且被线程使用之前),会执行类的初始化。在执行类的初始化期间,JVM会去获取一个锁。这个锁可以同步多个线程对同一个类的初始化。基于这个特性,可以实现另一种线程安全的延迟初始化方案。    
 

public class MyObject {
	private static MyObject myObject;
	/*私有构造函数避免被实例化*/
	private MyObject(){
		
	}
	//静态内部类方式
	private static class MyObjectHandler{
		private static MyObject myObject=new MyObject();
	}
	public static MyObject getInstance(){
		return MyObjectHandler.myObject;
	}
}

结果可行。

使用静态代码块实现单例模式

public class MyObject {
	private static MyObject instance;
	/*私有构造函数避免被实例化*/
	private MyObject(){
		
	}
	static{
		instance=new MyObject();
	}
	public static MyObject getInstance(){
		return instance;
	}
}

结果可行。 该方案的实质是,允许②和③进行重排序,但不允许非构造线程(此处是B线程)“看到”这个重排序。

四、总结

     单例模式分为懒汉式和饿汉式,饿汉式在多线程环境下是线程安全的;懒汉式在多线程环境下 是“非线程安全”的,可以通过synchronized同步方法和“双重检测”机制来保证懒汉式在多线程环境下的线程安全性。静态内部类实现单例模式和静态代码块从广义上说都是饿汉式的。

发布了45 篇原创文章 · 获赞 12 · 访问量 3万+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章