本文重要關注點:
- 執行緒安全的單例模式
- 防止物件克隆破壞單例模式Singleton
- 防止序列化破壞單例模式
單例模式
什麼是單例模式
單例模式屬於管理例項的創造型型別模式。單例模式保證在你的應用種最多隻有一個指定類的例項。
單例模式應用場景
- 專案配置類
讀取專案的配置資訊的類可以做成單例的,因為只需要讀取一次,且配置資訊欄位一般比較多節省資源。通過這個單例的類,可以對應用程式中的類進行全域性訪問。無需多次對配置檔案進行多次讀取。
- 應用日誌類
日誌器Logger在你的應用中是無處不在的。也應該只初始化一次,但是可以到處使用。
- 分析和報告類
如果你在使用一些資料分析工具例如Google Analytics。你就可以注意到它們被設計成單例的,僅僅初始化一次,然後在使用者的每一個行為中都可以使用。
實現單例模式的類
-
將預設的構造器設定為private。阻止其他類從應用中直接初始化該類。
-
建立一個public static 的靜態方法。該方法用於返回一個單例類例項。
-
還可以選擇懶載入初始化更友好。
示例程式碼
示例程式碼參見以下類
- org.byron4j.cookbook.designpattern.singleton.Singleton
public class Singleton {
private static Singleton instance;
// 構造器私有化
private Singleton(){
}
// 提供靜態方法
public static Singleton getInstance(){
// 懶載入初始化,在第一次使用時才建立例項
if(instance == null){
instance = new Singleton();
}
return instance;
}
public void display(){
System.out.println("Hurray! I am create as a Singleton!");
}
}
複製程式碼
單元測試類:
package org.byron4j.cookbook.designpattern;
import org.byron4j.cookbook.designpattern.singleton.Singleton;
import org.junit.Test;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class SingletonTest {
@Test
public void test(){
final Set<Singleton> sets = new HashSet<>();
ExecutorService es = Executors.newFixedThreadPool(10000);
for(int i = 1; i <= 100000; i++){
es.execute(new Runnable() {
public void run(){
Singleton s = Singleton.getInstance();
sets.add(s);
}
});
}
System.out.println(sets);
}
}
複製程式碼
執行輸出如下,結果生成了多個Singleton例項:
[org.byron4j.cookbook.designpattern.singleton.Singleton@46b91344, org.byron4j.cookbook.designpattern.singleton.Singleton@1f397b96]
執行緒安全的單例模式
執行緒安全對於單例類來說是非常重要的。上述Singleton類是非執行緒安全的,因為線上程併發的場景下,可能會建立多個Singleton例項。
為了規避這個問題,我們可以將 getInstance 方法用同步字 synchronized 修飾,這樣迫使執行緒等待直到前面一個執行緒執行完畢,如此就避免了同時存在多個執行緒訪問該方法的場景。
public static synchronized Singleton getInstance() {
// Lazy initialization, creating object on first use
if (instance == null) {
instance = new Singleton();
}
return instance;
}
複製程式碼
這樣確實解決了執行緒安全的問題。但是,synchronized
關鍵字存在嚴重的效能問題。我們還可以進一步優化 getInstance 方法,將例項同步,將方法範圍縮小:
public static Singleton getInstance() {
// Lazy initialization, creating object on first use
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
複製程式碼
單元測試三種方式耗時比較:
package org.byron4j.cookbook.designpattern;
import org.byron4j.cookbook.designpattern.singleton.Singleton;
import org.byron4j.cookbook.designpattern.singleton.SingletonSynchronized;
import org.byron4j.cookbook.designpattern.singleton.SingletonSynchronizedOptimized;
import org.junit.Test;
import java.util.HashSet;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class SingletonTest {
@Test
public void test(){
final Set<Singleton> sets = new HashSet<>();
long startTime = System.currentTimeMillis();
ExecutorService es = Executors.newFixedThreadPool(10000);
for(int i = 1; i <= 100000; i++){
es.execute(new Runnable() {
public void run(){
Singleton s = Singleton.getInstance();
sets.add(s);
}
});
}
System.out.println("test用時:" + (System.currentTimeMillis() - startTime));
System.out.println(sets);
}
@Test
public void testSynchronized(){
final Set<SingletonSynchronized> sets = new HashSet<>();
long startTime = System.currentTimeMillis();
ExecutorService es = Executors.newFixedThreadPool(10000);
for(int i = 1; i <= 100000; i++){
es.execute(new Runnable() {
public void run(){
SingletonSynchronized s = SingletonSynchronized.getInstance();
sets.add(s);
}
});
}
System.out.println("testSynchronized用時:" + (System.currentTimeMillis() - startTime));
System.out.println(sets);
}
@Test
public void testOptimised(){
final Set<SingletonSynchronizedOptimized> sets = new HashSet<>();
long startTime = System.currentTimeMillis();
ExecutorService es = Executors.newFixedThreadPool(10000);
for(int i = 1; i <= 100000; i++){
es.execute(new Runnable() {
public void run(){
SingletonSynchronizedOptimized s = SingletonSynchronizedOptimized.getInstance();
sets.add(s);
}
});
}
System.out.println("testOptimised用時:" + (System.currentTimeMillis() - startTime));
System.out.println(sets);
}
}
複製程式碼
執行測試用例,輸出如下:
test用時:1564
[org.byron4j.cookbook.designpattern.singleton.Singleton@68eae58e]
testSynchronized用時:3658
[org.byron4j.cookbook.designpattern.singleton.SingletonSynchronized@36429a46]
testOptimised用時:2254
[org.byron4j.cookbook.designpattern.singleton.SingletonSynchronizedOptimized@21571826]
複製程式碼
可以看到,最開始的實現方式效能是最好的,但是是非執行緒安全的; Synchronized 鎖住整個getInstance方法,可以做到執行緒安全,但是效能是最差的; 縮小Synchronized範圍,可以提高效能。
單例Singleton和物件克隆
涉及單例類時還要注意clone方法的正確使用:
package org.byron4j.cookbook.designpattern.singleton;
/**
* 單例模式例項
* 1. 構造器私有化
* 2. 提供靜態方法供外部獲取單例例項
* 3. 延遲初始化例項
*/
public class SingletonZClone implements Cloneable{
private static SingletonZClone instance;
// 構造器私有化
private SingletonZClone(){
}
// 提供靜態方法
public static SingletonZClone getInstance(){
// 將同步鎖範圍縮小,降低效能損耗
if(instance == null){
synchronized (SingletonZClone.class){
if(instance == null){
instance = new SingletonZClone();
}
}
}
return instance;
}
/**
* 克隆方法--改為public
* @return
* @throws CloneNotSupportedException
*/
@Override
public Object clone() throws CloneNotSupportedException {
return super.clone();
}
public void display(){
System.out.println("Hurray! I am create as a SingletonZClone!");
}
}
複製程式碼
預設情況下clone時protected修飾的,這裡改為了public修飾,測試用例如下:
@Test
public void testClone() throws CloneNotSupportedException {
SingletonZClone singletonZClone1 = SingletonZClone.getInstance();
SingletonZClone singletonZClone2 = SingletonZClone.getInstance();
SingletonZClone singletonZClone3 = (SingletonZClone)SingletonZClone.getInstance().clone();
System.out.println(singletonZClone1 == singletonZClone2);
System.out.println(singletonZClone1 == singletonZClone3);
System.out.println(singletonZClone2 == singletonZClone3);
}
複製程式碼
輸出如下:
true
false
false
我們瞭解一下clone方法的API解釋, clone 後的物件雖然屬性值可能是一樣的,但是已經不是同一個物件例項了:
x.clone() != x
x.clone().getClass() == x.getClass()
x.clone().equals(x)
clone方法返回一個被克隆物件的例項的副本,除了記憶體地址其他屬性值都是一樣的,所以副本和被克隆物件不是同一個例項。 可以看出clone方法破壞了單例類,為防止該問題出現,我們需要禁用clone方法,直接改為:
/**
* 克隆方法--改為public
* @return
* @throws CloneNotSupportedException
*/
@Override
public Object clone() throws CloneNotSupportedException {
throw new CloneNotSupportedException();
}
複製程式碼
單例和序列化問題
Java序列化機制允許將一個物件的狀態轉換為位元組流,就可以很容易地儲存和轉移。 一旦物件被序列化,你就可以對其進行反序列化--將位元組流轉為物件。 如果一個Singleton類被序列化,則可能建立重複的物件。 我們可以使用鉤子hook,來解釋這個問題。
readResolve()方法
在Java規範中有關於readResolve()方法的介紹:
對於可序列化的和外部化的類,readResolve() 方法允許一個類可以替換/解析從流中讀取到的物件。 通過實現 readResolve 方法,一個類就可以直接控制反序列化後的例項以及型別。 定義如下:
ANY-ACCESS-MODIFIER Object readResolve() throws ObjectStreamException; 複製程式碼
readResolve 方法會在ObjectInputStream 從流中讀取一個物件時呼叫。ObjectInputStream 會檢測類是否定義了 readResolve 方法。 如果 readResolve 方法定義了,會呼叫該方法用於指定從流中反序列化後作為返回的結果物件。 返回的型別要與原物件的型別一致,不然會出現 ClassCastException。
@Test
public void testSeria() throws Exception {
SingletonZCloneSerializable singletonZClone1 = SingletonZCloneSerializable.getInstance();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test.ser"));
oos.writeObject(singletonZClone1);
oos.close();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test.ser"));
SingletonZCloneSerializable test = (SingletonZCloneSerializable) ois.readObject();
ois.close();
System.out.println(singletonZClone1 == test);
}
複製程式碼
測試輸出: false; 說明反序列化的時候已經不是原來的例項了,如此會破壞單例模式。
所以我們可以覆蓋 readResolve 方法來解決序列化破壞單例的問題:
類 SingletonZCloneSerializableReadResolve 增加 readResolve 方法:
/**
* 反序列化時返回instance例項,防止破壞單例模式
* @return
*/
protected Object readResolve(){
return getInstance();
}
複製程式碼
執行測試用例:
@Test
public void testSReadResolve() throws Exception {
s = SingletonZCloneSerializableReadResolve.getInstance();
ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("test.ser"));
oos.writeObject(s);
oos.close();
ObjectInputStream ois = new ObjectInputStream(new FileInputStream("test.ser"));
SingletonZCloneSerializableReadResolve test = (SingletonZCloneSerializableReadResolve) ois.readObject();
ois.close();
System.out.println(s == test);
}
複製程式碼
輸出true,有效防止了反序列化對單例的破壞。
你知道嗎?
-
單例類是很少使用的,如果你要使用這個設計模式,你必須清楚的知道你在做什麼。因為全域性範圍內僅僅建立一個例項,所以在資源受約束的平臺是存在風險的。
-
注意物件克隆。 單例模式需要仔細檢查並阻止clone方法。
-
多執行緒訪問下,需要注意執行緒安全問題。
-
小心多重類載入器,也許會破壞你的單例類。
-
如果單例類是可序列化的,需要實現嚴格型別