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