Java實現多種單例模式SingletonPattern


糾結了好久,要不要寫一篇博客記錄自己學習單例模式的過程。網上相關博客多的很,好像沒什么必要重復造一個老輪子。

但是最近面試、看面試書,發現單例模式還是經常會被考到的,而且作為設計模式中相對來說比較簡單的一種,掌握好還是很有必要的。

而掌握知識的最好途徑不是看別人的文章,而是自己親手造一個,想必那樣體會更深。


好了廢話不多說。

單例模式的作用從名字上就可以看出來:保證某個類的實例只能被創建一次,以后都是調用這個實例

常見的用到單例模式的情況有:

1.windows下的任務管理器,只允許創建一個實例,否則同時打開多個任務管理器,這個里結束了某個進程,另外一個管理器中還可以看到,不是很安全;

2.Android中的Application一般情況下都是只有一個,需要在唯一的Application中創建一些成員變量;

3.總之,單例模式就是為了保證某個狀態的一致性。


明白了他的重要性后,就開始實現吧。

SingletonPattern有多種實現方式,網上最多的有七八種。不由得讓我想到茴香豆的七種寫法。同樣的專注,不同的是每一種寫法都是一種優化。

今天我要實現的是相對簡單的,容易理解的三種:

1.最簡單的“懶漢模式”:

package net.sxkeji.singnleton;

/**
* 按照四人團的說法:<br/>
* Singleton模式的工作方式是:保證一個類僅有一個實例,並提供一個訪問它的全局訪問點<br/>
* 懶漢單例模式適用於單線程情況下
*/
public class Singleton1 {
private static Singleton1 instance = null;
private String name;
/**
* 為了確保對象實例的唯一性,將構造函數定義為私有的、或者保護的
*/
private Singleton1(){
//一些操作..
}

private Singleton1(String str){
name = str;
}

/**
* 最簡單的實現<br/>
* 缺點:線程不安全,當多線程同時訪問時可能會導致創建多個實例
* @param str
* @returnSingleton1實例
*/
public static Singleton1 getInstance(String str){
if(instance == null){
instance = new Singleton1(str);
}
return instance;
}

public void showName(){
System.out.println(name);
}
}


構造 函數的訪問權限設置為private,是為了在類外部不能通過new一個類來創建實例。想要得到該類的對象只能通過getInstance方法。第一次創建實例時將instance設置為非空,以后就直接返回這個實例了。


測試方法:

public static void testSingleton1(){
Singleton1 first = Singleton1.getInstance("熊大");
Singleton1 second = Singleton1.getInstance("熊二");
first.showName();
second.showName();
}
運行結果:

可以看到在單線程下以后不管傳入什么參數,我們得到的都是首次創建時的實例。

但是在多線程下,兩個線程可能“同時”要創建這個類的對象,這時instance都是null,就會創建2個實例,這就是懶漢模式的弊端--線程不安全

測試方法:

/**
* 多線程情況下懶漢實現導致的問題
*/
public static void multiThreadSingleton1(){
Thread thread = new Thread(new Runnable() {

@Override
public void run() {
Singleton1 second = Singleton1.getInstance("熊二");
second.showName();
}
});
thread.start();
Thread thread2 = new Thread(new Runnable() {

@Override
public void run() {
Singleton1 second = Singleton1.getInstance("熊大");
second.showName();
}
});
thread2.start();
}

測試結果:



可以看到創建出了2個實例,這不是我們想要的結果。


2.懶漢模式的優化---加鎖控制的單例模式(江湖人稱double-checked-locking)

為了避免在多線程下創建多個實例的情況出現,我們需要進行鎖控制

有一種方法是直接在懶漢模式的基礎上,對getInstance方法前加個synchronized。這樣每次只允許一個線程訪問,保證了線程安全。

但是導致了一個新問題:如果有多線程同時訪問,其他線程都需要等到前一個訪問的線程對該方法的操作結束后才可以進行操作,2等1,3等2,4等3,得等到猴年馬月。時間上效率太低

所以需要進一步優化:當該類並沒有創建實例時才進行鎖控制,如果已經創建過實例,以后直接調用實例就好了,用不着再等待。

代碼如下:

/**
* double-checked-locking模式<br/>
* 將非必須的鎖定優化掉,避免因為同步鎖導致的大量等待時間<br/>
* 支持多線程環境
*/
public class Singleton2 {
private static Singleton2 instance = null;
private String name;
/**
* 為了確保對象實例的唯一性,將構造函數定義為私有的、或者保護的
*/
private Singleton2(){
//一些操作..
}

private Singleton2(String str){
name = str;
}

/**
* 2個 if 提高效率
*/
public static Singleton2 getInstance(String str){
//第一個檢查是否為空是為了在除首次創建時直接返回實例,避免等待加鎖浪費時間
if(instance == null){
//同步鎖避免【多線程同時創建多個實例】
doSync(str);
}
return instance;
}

/**
* 加了鎖的實例化操作
* @param str
*/
public synchronized static void doSync(String str){
if(instance == null)
instance = new Singleton2(str);
}

public void showName(){
System.out.println(name);
}
}
運行結果:





咦?雖然保證了實例的唯一性,但是為什么會出現2種結果呢?

這是因為該模式只能保證實例的唯一性,並沒有控制創建實例的先后順序。由於我測試方法是在兩個線程里,他倆創建實例的先后順序不確定,可能“熊大”的先創建,也可能“熊二”的先創建。

正常情況下我們是在主線程里對類對象的初始化創建,其他線程訪問我們創建好的。這樣上面的先后問題就不存在

如果需要在多線程里訪問,就需要在類方法創建時初始化,而不是調用時才初始化。由此引出了第三種,餓漢式單例。


3.餓漢式單例,根據static預先加載的特性

在實現這種單例時我發現自己對static的理解還不夠

被static修飾的成員變量和成員方法獨立於該類的任何對象。也就是說,它不依賴類特定的實例,被類的所有實例共享。只要這個類被加載,Java虛擬機就能根據類名在運行時數據區的方法區內定找到他們。因此,static對象可以在它的任何對象創建之前訪問,無需引用任何對象。

這種方式基於classloder機制避免了多線程的同步問題.在類加載的同時就已經創建好一個靜態的對象供系統使用,以后不再改變,所以是線程安全的。


代碼如下:

**
*餓漢式單例,根據static預先加載的特性<br/>
*餓漢式在類創建的同時就已經創建好一個靜態的對象供系統使用,以后不再改變,所以是線程安全的
*/
public class Singleton3 {
<span style="color:#ff0000;">private static final Singleton3 instance = new Singleton3("熊妹");</span>
private String name;
/**
* 為了確保對象實例的唯一性,將構造函數定義為私有的、或者保護的
*/
private Singleton3(){
//一些操作..
}

private Singleton3(String str){
name = str;
}

public static Singleton3 getInstance(String str){
return instance;
}

public void showName(){
System.out.println(name);
}
}


注意到我紅色標注的地方同時用static和final修飾了對象instance,他倆有什么作用呢?

對於被static和final修飾過的實例常量,實例本身不能再改變了。也就是以后都不允許修改,保證了一致性。

測試用例和上面多線程訪問的那個方法一樣。

運行結果:


可以看到在類加載時就創建對象並初始化,以后得到的都是這唯一的實例。

不過我覺得有個缺點,不能從外部初始化,都是寫死的。

但是看了很多博客,大牛們都推薦這種方法,看來還是相對來說算是性能比較好的。



總結:

菜鳥還是重復造輪子學得多,好記性不如敲鍵盤,況且在自己實現的過程中還可以發現其他的問題,比如我對static的不理解,多線程的不熟悉等等。總之是收獲很多。下次再實現別的模式,這一階段還是趕緊把Java基礎和算法夯實了。


張拭心記於  2015/7/21 http://blog.csdn.net/u011240877


注意!

本站转载的文章为个人学习借鉴使用,本站对版权不负任何法律责任。如果侵犯了您的隐私权益,请联系我们删除。



 
粤ICP备14056181号  © 2014-2021 ITdaan.com