1. 程式人生 > >Java多執行緒學習筆記21之單例模式與多執行緒

Java多執行緒學習筆記21之單例模式與多執行緒

詳細程式碼見:github程式碼地址

 

第六章

單例模式與多執行緒

前言:
我之前已經開設了23個設計模式這個專欄,介紹了很多的Java設計模式,其中一些模式對於絕
大多數程式語言設計思想都是類似的,需要了解單例模式的可以去看看。
我們在實際開發中經常用到單例模式,但是同時也配合多執行緒來使用,我們需要考慮的是
如何使單例模式遇到多執行緒是安全的、正確的。


單例模式:

1. 單例類只能有一個例項
2. 單例類必須自己建立自己的唯一例項
3. 單例類必須給其它物件提供這一例項


單例模式的應用:

單例模式的應用非常廣泛,例如在計算機系統中執行緒池、快取、日誌物件、對話方塊、印表機、顯
卡的驅動程式物件常被設計成單例。這些應用都或多或少具有資源管理器的功能。
單例物件通常作為程式中的存放配置資訊的載體,因為它能保證其它物件讀到一致的資訊。例
如在某個伺服器程式中,該伺服器的配置資訊可能放在資料庫或檔案中(json,xml,txt比較
常見),這些配置資料由某個單例物件統一獲取,服務程序中的其他物件如果要獲取這些配置
資訊,只需訪問該單例物件即可。


在這裡簡單來說一下Java中類的載入:
所有的類都是在對其第一次使用時,動態載入到JVM中的(懶載入)。當程式建立第一個對類的
靜態成員的引用時,就會載入這個類。使用new建立類物件的時候也會被當作對類的靜態成員的
引用。因此java程式程式在它開始執行之前並非被完全載入,其各個類都是在必需時才載入的。
這一點與許多傳統語言都不同。動態載入使能的行為,在諸如C++這樣的靜態載入語言中是很難
或者根本不可能複製的。

  在類載入階段,類載入器首先檢查這個類的Class物件是否已經被載入。如果尚未載入,預設
的類載入器就會根據類的全限定名查詢.class檔案。在這個類的位元組碼被載入時,它們會接受驗
證,以確保其沒有被破壞,並且不包含不良java程式碼。一旦某個類的Class物件被載入記憶體,我
們就可以它來建立這個類的所有物件。


1. 立即載入/"餓漢模式"
立即載入:

    立即載入就是使用類的時候已經將物件建立完畢,常見的實現辦法就是直接new例項化。
而立即載入從中文的語境來看,有"著急"、"急迫"的含義,所以也稱為"餓漢模式"
立即載入/"餓漢模式"是在呼叫方法前,例項已經被建立了.


舉例:

package chapter06.section01.project_1_singleton_0;

public class MyObject {
	
	//立即載入方式==餓漢模式
	private static MyObject myObject = new MyObject();
	
	private MyObject() {}
	
	public static MyObject getInstance() {
		//此程式碼版本為立即載入
		//此版本程式碼的缺點是不能有其他例項變數
		//因為getInstance()方法沒有同步
		//所以有可能出現非執行緒完全問題
		return myObject;
	}
}


package chapter06.section01.project_1_singleton_0;

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


package chapter06.section01.project_1_singleton_0;

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();
	}
}
/*
result:
1122949189
1122949189
1122949189
*/

列印的hashCode是同一個值,說明物件是同一個,也就實現了立即載入型單例設計模式

2. 延遲載入/"懶漢模式"
延遲載入:

    延遲載入就是在呼叫get()方法時例項才被建立,常見的實現辦法就是在get()方法中進行
new例項化。而延遲載入從中文的語境來看,是"緩慢"、"不急迫"的含義,所以也稱為"懶漢模式"


(1) 延遲載入/"餓漢模式"解析
是在呼叫方法時例項才被建立
 

package chapter06.section02.project_1_singleton_1;

public class MyObject {
	private static MyObject myObject;
	
	//注意此處為private
	private MyObject() {}
	
	public static MyObject getInstance() {
		//延遲載入
		if(myObject != null) {
		} else {
			//模擬在建立物件之前做一些準備性工作
//			Thread.sleep(3000); //要捕獲異常
			myObject = new MyObject();
		}
		
		return myObject;
	}
}


package chapter06.section02.project_1_singleton_1;

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


package chapter06.section02.project_1_singleton_1;

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();
	}
}
/*
單執行緒情況下result:
781844118
多執行緒情況下加上註釋result:
1456203498
1455173736
2038812851
*/

只取得一個物件的例項,但如果是在多執行緒的環境中,就會出現取出多個例項的情況,與單
例模式的初衷是背離的

(2) 延遲載入/"懶漢模式"的缺點
如果是在多執行緒的環境中,前面"延遲載入"示例程式碼完全就是錯誤的,根本不能實現保持單
例的狀態
舉例:
上面程式碼去掉註釋,列印了3種hashCode,說明建立了3個物件,並不是單例的。


(3) 延遲載入/"懶漢模式"的解決方案

1) 宣告synchronized關鍵字
既然多個執行緒可以同時進入getInstance()方法,那麼只需要對getInstance()方法宣告
synchronized關鍵字即可。


舉例:

package chapter06.section02.project_3_singleton_2_1;

public class MyObject {
	private static MyObject myObject;
	
	//注意此處為private
	private MyObject() {}
	
	//設定同步方法效率太低了
	//整個方法被上鎖, static靜態方法獲得的是MyObject的class物件鎖,只有一個
	synchronized public static MyObject getInstance() {
		try {
			if(myObject != null) {
			} else {
				//模擬在建立物件之前做一些準備性的工作
				Thread.sleep(3000);
				myObject = new MyObject();
			}
		} catch (InterruptedException e) {
			// TODO: handle exception
			e.printStackTrace();
		}
		
		return myObject;
	}
}


package chapter06.section02.project_3_singleton_2_1;

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


package chapter06.section02.project_3_singleton_2_1;

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();
	}
}
/*
reslt:
1566790350
1566790350
1566790350
*/

此方法加入同步synchronized關鍵字得到相同例項的物件,但此種方法的執行效率非常低
下,是同步執行的,下一個執行緒想要獲取的物件,必須等上一個執行緒釋放鎖之後,才可以
繼續執行。


2) 嘗試同步程式碼塊
同步方法是對方法的整體進行持鎖,這對執行效率來講是不利的,改為同步程式碼塊

package chapter06.section02.project_4_singleton_2_2;

public class MyObject {
	private static MyObject myObject;
	
	//注意此處為private
	private MyObject() {}
	
	 public static MyObject getInstance() {
		try {
			//此種寫法等同於:
			//synchronized public static MyObject getInstance()
			//的寫法,效率一樣很低,全部程式碼被上鎖
			synchronized(MyObject.class) {
				if(myObject != null) {
				} else {
					//模擬在建立物件之前做一些準備性的工作
					Thread.sleep(3000);
					myObject = new MyObject();
				}
			}
		} catch (InterruptedException e) {
			// TODO: handle exception
			e.printStackTrace();
		}
		
		return myObject;
	}
}


package chapter06.section02.project_4_singleton_2_2;

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


package chapter06.section02.project_4_singleton_2_2;

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();
		
		// 此版本程式碼雖然是正確的
		// 但public static MyObject getInstance()方法
		// 中的全部程式碼都是同步的了,這樣做有損效率
	}
}
/*
reslt:
1122949189
1122949189
1122949189
*/

 

3) 針對某些重要的程式碼進行單獨的同步
同步程式碼塊可以針對某些重要的程式碼進行單獨的同步,而其他的程式碼則不需要同步。這樣在
執行時,效率完全可以得到大幅度提升。

package chapter06.section02.project_5_singleton_3;

public class MyObject {
	private static MyObject myObject;
	
	//注意此處為private
	private MyObject() {}
	
	public static MyObject getInstance() {
		try {
			if (myObject != null) {
			} else {
				// 模擬在建立物件之前做一些準備性的工作
				Thread.sleep(3000);
				// 使用synchronized (MyObject.class)
				// 雖然部分程式碼被上鎖
				// 但還是有非執行緒安全問題
				synchronized (MyObject.class) {
					myObject = new MyObject();
				}
			}
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
		return myObject;
	}
}


package chapter06.section02.project_5_singleton_3;

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


package chapter06.section02.project_5_singleton_3;

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();
		
		// 此版本程式碼雖然是正確的
		// 但public static MyObject getInstance()方法
		// 中的全部程式碼都是同步的了,這樣做有損效率
	}
}
/*
reslt:
1122949189
1477182363
1566790350
*/

雖然提升了效率,但問題還是沒有解決(顯然的,寫部落格好費時間)

4) 使用DCL雙檢查鎖機制(Double-checked Lock, DCL)

DCL雙檢查機制:
    就是在同步程式碼塊呼叫之前檢查一遍,再在同步程式碼塊內部再檢查一遍,雙重保險

舉例:

package chapter06.section02.project_6_singleton_5;

public class MyObject {
	private static MyObject myObject;
	
	//注意此處為private
	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;
	}
	
	// 此版本的程式碼稱為:
	// 雙重檢查Double-Check Locking
}


package chapter06.section02.project_6_singleton_5;

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


package chapter06.section02.project_6_singleton_5;

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();
	}
}
/*
reslt:
1403299602
1403299602
1403299602
*/

DCL是大多數多執行緒結合單例模式使用的解決方案


3. 使用靜態內建類實現單例模式

DCL可以解決多執行緒單例模式的非執行緒安全問題。當然,使用其他的辦法也能達到同樣的效
果。

舉例:

package chapter06.section03.project_1_singleton_7;

public class MyObject {
	
	//內部類方式
	private static class MyObjectHandler{
		private static MyObject myObject = new MyObject();
	}
	
	private MyObject() {}
	
	public static MyObject getInstance() {
		return MyObjectHandler.myObject;
	}
}


package chapter06.section03.project_1_singleton_7;

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();
	}
}
/*
reslt:
1391749355
1391749355
1391749355
*/


4.序列化與反序列化的單例模式實現

Java提供了一種物件序列化的機制,該機制中,一個物件可以被表示為一個位元組序列,
該位元組序列包括該物件的資料、有關物件的型別的資訊和儲存在物件中資料的型別。
當一個類實現了Serializable介面,我們就可以把序列化物件寫入檔案之後,從檔案
中讀取出來。從記憶體讀出而組裝的物件破壞了單例的規則,單例是要求一個JVM中只有
一個類物件的,而現在通過反序列化,一個新的物件克隆了出來。

靜態內之類可以達到執行緒安全問題,但如果遇到序列化物件時,使用預設的方式執行得
到的結果還是多例的。

舉例:

package chapter06.section04.project_1_singleton_7_1;

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

public class MyObject implements Serializable{
	
	private static final long serialVersionUID = 888L;
	
	//內部類方式
	private static class MyObjectHandler{
		private static final MyObject myObject = new MyObject();
	}
	
	private MyObject() {}
	
	public static MyObject getInstance() {
		return MyObjectHandler.myObject;
	}
	
	protected Object readResolve() throws ObjectStreamException{
		System.out.println("呼叫了readResolve方法!");
		return MyObjectHandler.myObject;
	}
	
}


package chapter06.section04.project_1_singleton_7_1;

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 SaveAndRead {
	public static void main(String[] args) {
		try {
			MyObject myObject = MyObject.getInstance();
			FileOutputStream fosRef = new FileOutputStream(new File(
					"myObjectFile.txt"));
			ObjectOutputStream oosRef = new ObjectOutputStream(fosRef);
			oosRef.writeObject(myObject);
			oosRef.close();
			fosRef.close();
			System.out.println(myObject.hashCode());
		} catch (FileNotFoundException e) {
			// TODO: handle exception
			e.printStackTrace();
		} catch(IOException e) {
			e.printStackTrace();
		}
		
		try {
			FileInputStream fisRef = new FileInputStream(new File(
					"myObjectFile.txt"));
			ObjectInputStream oisRef = new ObjectInputStream(fisRef);
			MyObject myObject = (MyObject)oisRef.readObject();			
			oisRef.close();
			fisRef.close();
			System.out.println(myObject.hashCode());
		} catch (FileNotFoundException e) {
			// TODO: handle exception
			e.printStackTrace();
		} catch(IOException e) {
			e.printStackTrace();
		} catch(ClassNotFoundException e) {
			e.printStackTrace();
		}
	}
}
/*
result:
1612799726
787387795
去掉註釋:
1612799726
呼叫了readResolve方法!
1612799726
*/

 

5. 使用static程式碼塊實現單例模式

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

package chapter06.section05.project_1_singleton_8;

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


package chapter06.section05.project_1_singleton_8;

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


package chapter06.section05.project_1_singleton_8;

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();
	}
}
/*
reslt:
1122949189
1122949189
1122949189
*/

 

6. 使用enum列舉資料型別實現單例模式

列舉enum和靜態程式碼塊的特性相似,在使用列舉類時,構造方法會被自動呼叫,也可
以應用其這個特性實現單例設計模式


列舉類簡介:

    實質上定義出來的型別繼承自Java.lang.Enum型別(因此不能繼承其他的類),在
使用關鍵字enum建立列舉型別並編譯後,編譯器會為我們生成一個相關的類,這個類
繼承了Java API中的java.lang.Enum類,也就是說通過關鍵字enum建立列舉型別在
編譯後事實上也是一個類型別而且該類繼承自java.lang.Enum類
列舉的成員其實就是我們定義的列舉型別的一個例項Instance,被預設為public static 
final的成員常量。所以無法改變他們,他們是static成員,可以直接通過類名使用。
    列舉型別是單例模式的,建構函式是private,防止使用者生成例項,破壞唯一性

舉例:

package chapter06.section06.project_1_singleton_9;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public enum MyObject {
	connectionFactory;
	
	private Connection connection;
	
	private MyObject() {
		try {
			System.out.println("呼叫了MyObject的構造");
			String url = "jdbc:sqlserver://localhost:1079;databaseName=ghydb";
			String username = "sa";
			String password = "";
			String driverName = "com.microsoft.sqlserver.jdbc.SQLServerDriver";
			Class.forName(driverName);
			connection = DriverManager.getConnection(url,  username, password);
		} catch (ClassNotFoundException e) {
			// TODO: handle exception
			e.printStackTrace();
		} catch(SQLException e) {
			e.printStackTrace();
		}
	}
	
	public Connection getConnection() {
		return connection;
	}
}


package chapter06.section06.project_1_singleton_9;

public class MyThread extends Thread {

	@Override
	public void run() {
		for (int i = 0; i < 5; i++) {
			System.out.println(MyObject.connectionFactory.getConnection()
					.hashCode());
		}
	}
}


package chapter06.section06.project_1_singleton_9;

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();
	}
}

 

7. 完善使用enum列舉實現單例模式
前面一節對列舉類進行暴露,違反了"職責單一原則",完善


package chapter06.section07.project_1_singleton_10;

import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

public class MyObject {
	
	public enum MyEnumSingleton {
		connectionFactory;

		private Connection connection;

		private MyEnumSingleton() {
			try {
				System.out.println("建立MyObject物件");
				String url = "jdbc:sqlserver://localhost:1079;databaseName=y2";
				String username = "sa";
				String password = "";
				String driverName = "com.microsoft.sqlserver.jdbc.SQLServerDriver";
				Class.forName(driverName);
				connection = DriverManager.getConnection(url, username,
						password);
			} catch (ClassNotFoundException e) {
				e.printStackTrace();
			} catch (SQLException e) {
				e.printStackTrace();
			}
		}

		public Connection getConnection() {
			return connection;
		}
	}

	public static Connection getConnection() {
		return MyEnumSingleton.connectionFactory.getConnection();
	}
}


package chapter06.section07.project_1_singleton_10;

public class MyThread extends Thread {

	@Override
	public void run() {
		for (int i = 0; i < 5; i++) {
			System.out.println(MyObject.getConnection()
					.hashCode());
		}
	}
}


package chapter06.section07.project_1_singleton_10;

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();
	}
}