1. 程式人生 > >高併發下執行緒安全的單例模式(最全最經典)

高併發下執行緒安全的單例模式(最全最經典)

在所有的設計模式中,單例模式是我們在專案開發中最為常見的設計模式之一,而單例模式有很多種實現方式,你是否都瞭解呢?高併發下如何保證單例模式的執行緒安全性呢?如何保證序列化後的單例物件在反序列化後任然是單例的呢?這些問題在看了本文之後都會一一的告訴你答案,趕快來閱讀吧!

什麼是單例模式?

在文章開始之前我們還是有必要介紹一下什麼是單例模式。單例模式是為確保一個類只有一個例項,併為整個系統提供一個全域性訪問點的一種模式方法。

從概念中體現出了單例的一些特點:

(1)、在任何情況下,單例類永遠只有一個例項存在

(2)、單例需要有能力為整個系統提供這一唯一例項 

為了便於讀者更好的理解這些概念,下面給出這麼一段內容敘述:

在計算機系統中,執行緒池、快取、日誌物件、對話方塊、印表機、顯示卡的驅動程式物件常被設計成單例。這些應用都或多或少具有資源管理器的功能。每臺計算機可以有若干個印表機,但只能有一個Printer Spooler,以避免兩個列印作業同時輸出到印表機中。每臺計算機可以有若干通訊埠,系統應當集中管理這些通訊埠,以避免一個通訊埠同時被兩個請求同時呼叫。總之,選擇單例模式就是為了避免不一致狀態,避免政出多頭。

正是由於這個特點,單例物件通常作為程式中的存放配置資訊的載體,因為它能保證其他物件讀到一致的資訊。例如在某個伺服器程式中,該伺服器的配置資訊可能存放在資料庫或檔案中,這些配置資料由某個單例物件統一讀取,服務程序中的其他物件如果要獲取這些配置資訊,只需訪問該單例物件即可。這種方式極大地簡化了在複雜環境 下,尤其是多執行緒環境下的配置管理,但是隨著應用場景的不同,也可能帶來一些同步問題。
    

各式各樣的單例實現

溫馨提示:本文敘述中涉及到的相關原始碼可以在這裡進行下載原始碼,讀者可免積分下載。

1、餓漢式單例

餓漢式單例是指在方法呼叫前,例項就已經建立好了。下面是實現程式碼:

package org.mlinge.s01;

public class MySingleton {
	
	private static MySingleton instance = new MySingleton();
	
	private MySingleton(){}
	
	public static MySingleton getInstance() {
		return instance;
	}
	
}
以上是單例的餓漢式實現,我們來看看餓漢式在多執行緒下的執行情況,給出一段多執行緒的執行程式碼:
package org.mlinge.s01;

public class MyThread extends Thread{
  	
	@Override
	public void run() { 
		System.out.println(MySingleton.getInstance().hashCode());
	}
	
	public static void main(String[] args) { 
		
		MyThread[] mts = new MyThread[10];
		for(int i = 0 ; i < mts.length ; i++){
			mts[i] = new MyThread();
		}
		
		for (int j = 0; j < mts.length; j++) {
			mts[j].start();
		}
	}
}

以上程式碼執行結果:

1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954

從執行結果可以看出例項變數額hashCode值一致,這說明物件是同一個,餓漢式單例實現了。

2、懶漢式單例

懶漢式單例是指在方法呼叫獲取例項時才建立例項,因為相對餓漢式顯得“不急迫”,所以被叫做“懶漢模式”。下面是實現程式碼:

package org.mlinge.s02;

public class MySingleton {
	
	private static MySingleton instance = null;
	
	private MySingleton(){}
	
	public static MySingleton getInstance() {
		if(instance == null){//懶漢式
			instance = new MySingleton();
		}
		return instance;
	}
}
這裡實現了懶漢式的單例,但是熟悉多執行緒併發程式設計的朋友應該可以看出,在多執行緒併發下這樣的實現是無法保證例項例項唯一的,甚至可以說這樣的失效是完全錯誤的,下面我們就來看一下多執行緒併發下的執行情況,這裡為了看到效果,我們對上面的程式碼做一小點修改:
package org.mlinge.s02;

public class MySingleton {
	
	private static MySingleton instance = null;
	
	private MySingleton(){}
	
	public static MySingleton getInstance() {
		try { 
			if(instance != null){//懶漢式 
				
			}else{
				//建立例項之前可能會有一些準備性的耗時工作 
				Thread.sleep(300);
				instance = new MySingleton();
			}
		} catch (InterruptedException e) { 
			e.printStackTrace();
		}
		return instance;
	}
}
這裡假設在建立例項前有一些準備性的耗時工作要處理,多執行緒呼叫:
package org.mlinge.s02;

public class MyThread extends Thread{
  	
	@Override
	public void run() { 
		System.out.println(MySingleton.getInstance().hashCode());
	}
	
	public static void main(String[] args) { 
		
		MyThread[] mts = new MyThread[10];
		for(int i = 0 ; i < mts.length ; i++){
			mts[i] = new MyThread();
		}
		
		for (int j = 0; j < mts.length; j++) {
			mts[j].start();
		}
	}
}

執行結果如下:

1210420568
1210420568
1935123450
1718900954
1481297610
1863264879
369539795
1210420568
1210420568
602269801

從這裡執行結果可以看出,單例的執行緒安全性並沒有得到保證,那要怎麼解決呢?

3、執行緒安全的懶漢式單例

要保證執行緒安全,我們就得需要使用同步鎖機制,下面就來看看我們如何一步步的解決 存線上程安全問題的懶漢式單例(錯誤的單例)。

(1)、 方法中宣告synchronized關鍵字

出現非執行緒安全問題,是由於多個執行緒可以同時進入getInstance()方法,那麼只需要對該方法進行synchronized的鎖同步即可:

package org.mlinge.s03;

public class MySingleton {
	
	private static MySingleton instance = null;
	
	private MySingleton(){}
	
	public synchronized static MySingleton getInstance() {
		try { 
			if(instance != null){//懶漢式 
				
			}else{
				//建立例項之前可能會有一些準備性的耗時工作 
				Thread.sleep(300);
				instance = new MySingleton();
			}
		} catch (InterruptedException e) { 
			e.printStackTrace();
		}
		return instance;
	}
}

此時任然使用前面驗證多執行緒下執行情況的MyThread類來進行驗證,將其放入到org.mlinge.s03包下執行,執行結果如下:

1689058373
1689058373
1689058373
1689058373
1689058373
1689058373
1689058373
1689058373
1689058373
1689058373

從執行結果上來看,問題已經解決了,但是這種實現方式的執行效率會很低。同步方法效率低,那我們考慮使用同步程式碼塊來實現:

(2)、 同步程式碼塊實現

package org.mlinge.s03;

public class MySingleton {
	
	private static MySingleton instance = null;
	
	private MySingleton(){}
	
	//public synchronized static MySingleton getInstance() {
	public static MySingleton getInstance() {
		try { 
			synchronized (MySingleton.class) {
				if(instance != null){//懶漢式 
					
				}else{
					//建立例項之前可能會有一些準備性的耗時工作 
					Thread.sleep(300);
					instance = new MySingleton();
				}
			}
		} catch (InterruptedException e) { 
			e.printStackTrace();
		}
		return instance;
	}
}
這裡的實現能夠保證多執行緒併發下的執行緒安全性,但是這樣的實現將全部的程式碼都被鎖上了,同樣的效率很低下。

(3)、 針對某些重要的程式碼來進行單獨的同步(可能非執行緒安全)

針對某些重要的程式碼進行單獨的同步,而不是全部進行同步,可以極大的提高執行效率,我們來看一下:

package org.mlinge.s04;

public class MySingleton {
	
	private static MySingleton instance = null;
	
	private MySingleton(){}
	 
	public static MySingleton getInstance() {
		try {  
			if(instance != null){//懶漢式 
				
			}else{
				//建立例項之前可能會有一些準備性的耗時工作 
				Thread.sleep(300);
				synchronized (MySingleton.class) {
					instance = new MySingleton();
				}
			} 
		} catch (InterruptedException e) { 
			e.printStackTrace();
		}
		return instance;
	}
}
此時同樣使用前面驗證多執行緒下執行情況的MyThread類來進行驗證,將其放入到org.mlinge.s04包下執行,執行結果如下:
1481297610
397630378
1863264879
1210420568
1935123450
369539795
590202901
1718900954
1689058373
602269801
從執行結果來看,這樣的方法進行程式碼塊同步,程式碼的執行效率是能夠得到提升,但是卻沒能保住執行緒的安全性。看來還得進一步考慮如何解決此問題。

(4)、 Double Check Locking 雙檢查鎖機制(推薦)

為了達到執行緒安全,又能提高程式碼執行效率,我們這裡可以採用DCL的雙檢查鎖機制來完成,程式碼實現如下:

package org.mlinge.s05;

public class MySingleton {
	
	//使用volatile關鍵字保其可見性
	volatile private static MySingleton instance = null;
	
	private MySingleton(){}
	 
	public static MySingleton getInstance() {
		try {  
			if(instance != null){//懶漢式 
				
			}else{
				//建立例項之前可能會有一些準備性的耗時工作 
				Thread.sleep(300);
				synchronized (MySingleton.class) {
					if(instance == null){//二次檢查
						instance = new MySingleton();
					}
				}
			} 
		} catch (InterruptedException e) { 
			e.printStackTrace();
		}
		return instance;
	}
}
將前面驗證多執行緒下執行情況的MyThread類放入到org.mlinge.s05包下執行,執行結果如下:
369539795
369539795
369539795
369539795
369539795
369539795
369539795
369539795
369539795
369539795
從執行結果來看,該中方法保證了多執行緒併發下的執行緒安全性。

這裡在宣告變數時使用了volatile關鍵字來保證其執行緒間的可見性;在同步程式碼塊中使用二次檢查,以保證其不被重複例項化。集合其二者,這種實現方式既保證了其高效性,也保證了其執行緒安全性。

4、使用靜態內建類實現單例模式

DCL解決了多執行緒併發下的執行緒安全問題,其實使用其他方式也可以達到同樣的效果,程式碼實現如下:

package org.mlinge.s06;

public class MySingleton {
	
	//內部類
	private static class MySingletonHandler{
		private static MySingleton instance = new MySingleton();
	} 
	
	private MySingleton(){}
	 
	public static MySingleton getInstance() { 
		return MySingletonHandler.instance;
	}
}
以上程式碼就是使用靜態內建類實現了單例模式,這裡將前面驗證多執行緒下執行情況的MyThread類放入到org.mlinge.s06包下執行,執行結果如下:
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
從執行結果來看,靜態內部類實現的單例在多執行緒併發下單個例項得到了保證。

5、序列化與反序列化的單例模式實現

靜態內部類雖然保證了單例在多執行緒併發下的執行緒安全性,但是在遇到序列化物件時,預設的方式執行得到的結果就是多例的。

程式碼實現如下:

package org.mlinge.s07;

import java.io.Serializable;

public class MySingleton implements Serializable {
	 
	private static final long serialVersionUID = 1L;

	//內部類
	private static class MySingletonHandler{
		private static MySingleton instance = new MySingleton();
	} 
	
	private MySingleton(){}
	 
	public static MySingleton getInstance() { 
		return MySingletonHandler.instance;
	}
}
序列化與反序列化測試程式碼:
package org.mlinge.s07;

import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;

public class SaveAndReadForSingleton {
	
	public static void main(String[] args) {
		MySingleton singleton = MySingleton.getInstance();
		
		File file = new File("MySingleton.txt");
		
		try {
			FileOutputStream fos = new FileOutputStream(file);
			ObjectOutputStream oos = new ObjectOutputStream(fos);
			oos.writeObject(singleton);
			fos.close();
			oos.close();
			System.out.println(singleton.hashCode());
		} catch (FileNotFoundException e) { 
			e.printStackTrace();
		} catch (IOException e) { 
			e.printStackTrace();
		}
		
		try {
			FileInputStream fis = new FileInputStream(file);
			ObjectInputStream ois = new ObjectInputStream(fis);
			MySingleton rSingleton = (MySingleton) ois.readObject();
			fis.close();
			ois.close();
			System.out.println(rSingleton.hashCode());
		} catch (FileNotFoundException e) { 
			e.printStackTrace();
		} catch (IOException e) { 
			e.printStackTrace();
		} catch (ClassNotFoundException e) { 
			e.printStackTrace();
		}
		
	}
}
執行以上程式碼,得到的結果如下:
865113938
1442407170
從結果中我們發現,序列號物件的hashCode和反序列化後得到的物件的hashCode值不一樣,說明反序列化後返回的物件是重新例項化的,單例被破壞了。那怎麼來解決這一問題呢?

解決辦法就是在反序列化的過程中使用readResolve()方法,單例實現的程式碼如下:

package org.mlinge.s07;

import java.io.ObjectStreamException;
import java.io.Serializable;

public class MySingleton implements Serializable {
	 
	private static final long serialVersionUID = 1L;

	//內部類
	private static class MySingletonHandler{
		private static MySingleton instance = new MySingleton();
	} 
	
	private MySingleton(){}
	 
	public static MySingleton getInstance() { 
		return MySingletonHandler.instance;
	}
	
	//該方法在反序列化時會被呼叫,該方法不是介面定義的方法,有點兒約定俗成的感覺
	protected Object readResolve() throws ObjectStreamException {
		System.out.println("呼叫了readResolve方法!");
		return MySingletonHandler.instance; 
	}
}

再次執行上面的測試程式碼,得到的結果如下:
865113938
呼叫了readResolve方法!
865113938
從執行結果可知,新增readResolve方法後反序列化後得到的例項和序列化前的是同一個例項,單個例項得到了保證。

6、使用static程式碼塊實現單例

靜態程式碼塊中的程式碼在使用類的時候就已經執行了,所以可以應用靜態程式碼塊的這個特性的實現單例設計模式。

package org.mlinge.s08;

public class MySingleton{
	 
	private static MySingleton instance = null;
	 
	private MySingleton(){}

	static{
		instance = new MySingleton();
	}
	
	public static MySingleton getInstance() { 
		return instance;
	} 
}
測試程式碼如下:
package org.mlinge.s08;

public class MyThread extends Thread{
  	
	@Override
	public void run() { 
		for (int i = 0; i < 5; i++) {
			System.out.println(MySingleton.getInstance().hashCode());
		}
	}
	
	public static void main(String[] args) { 
		
		MyThread[] mts = new MyThread[3];
		for(int i = 0 ; i < mts.length ; i++){
			mts[i] = new MyThread();
		}
		
		for (int j = 0; j < mts.length; j++) {
			mts[j].start();
		}
	}
}
執行結果如下:
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
1718900954
從執行結果看,單例的執行緒安全性得到了保證。

7、使用列舉資料型別實現單例模式

列舉enum和靜態程式碼塊的特性相似,在使用列舉時,構造方法會被自動呼叫,利用這一特性也可以實現單例:

package org.mlinge.s09;

public enum EnumFactory{ 
    
    singletonFactory;
    
    private MySingleton instance;
    
    private EnumFactory(){//列舉類的構造方法在類載入是被例項化
        instance = new MySingleton();
    }
        
    public MySingleton getInstance(){
        return instance;
    }
    
}

class MySingleton{//需要獲實現單例的類,比如資料庫連線Connection
    public MySingleton(){} 
}
測試程式碼如下:
package org.mlinge.s09;

public class MyThread extends Thread{
  	
	@Override
	public void run() { 
		System.out.println(EnumFactory.singletonFactory.getInstance().hashCode());
	}
	
	public static void main(String[] args) { 
		
		MyThread[] mts = new MyThread[10];
		for(int i = 0 ; i < mts.length ; i++){
			mts[i] = new MyThread();
		}
		
		for (int j = 0; j < mts.length; j++) {
			mts[j].start();
		}
	}
}
執行後得到的結果:
1481297610
1481297610
1481297610
1481297610
1481297610
1481297610
1481297610
1481297610
1481297610
1481297610
執行結果表明單例得到了保證,但是這樣寫列舉類被完全暴露了,據說違反了“職責單一原則”,那我們來看看怎麼進行改造呢。

8、完善使用enum列舉實現單例模式

不暴露列舉類實現細節的封裝程式碼如下:

package org.mlinge.s10;

public class ClassFactory{ 
	
	private enum MyEnumSingleton{
		singletonFactory;
		
		private MySingleton instance;
		
		private MyEnumSingleton(){//列舉類的構造方法在類載入是被例項化
			instance = new MySingleton();
		}
 
		public MySingleton getInstance(){
			return instance;
		}
	} 
 
	public static MySingleton getInstance(){
		return MyEnumSingleton.singletonFactory.getInstance();
	}
}

class MySingleton{//需要獲實現單例的類,比如資料庫連線Connection
	public MySingleton(){} 
}

驗證單例實現的程式碼如下:
package org.mlinge.s10;

public class MyThread extends Thread{
  	
	@Override
	public void run() { 
		System.out.println(ClassFactory.getInstance().hashCode());
	}
	
	public static void main(String[] args) { 
		
		MyThread[] mts = new MyThread[10];
		for(int i = 0 ; i < mts.length ; i++){
			mts[i] = new MyThread();
		}
		
		for (int j = 0; j < mts.length; j++) {
			mts[j].start();
		}
	}
}
驗證結果:
1935123450
1935123450
1935123450
1935123450
1935123450
1935123450
1935123450
1935123450
1935123450
1935123450
驗證結果表明,完善後的單例實現更為合理。

以上就是本文要介紹的所有單例模式的實現,相信認真閱讀的讀者都已經明白文章開頭所引入的那幾個問題了,祝大家讀得開心:-D!

備註:本文的編寫思路和例項原始碼參照《Java多執行緒程式設計核心技術》-(高洪巖)一書中第六章的學習案例撰寫。