單例模式與多線程

一、前言

       如何使單例模式遇到多線程是安全的、正確的?

       我們在學習設計模式的時候知道單例模式有懶漢式和餓漢式之分。簡單來說,餓漢式就是在使用類的時候已經將對象創建完畢,懶漢式就是在真正調用的時候進行實例化操作。

二、餓漢式+多線程

單例:

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萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章