聲明:原創作品,轉載請注明出處http://www.lxweimin.com/p/b99e870f4ce0
有的時候,我們需要某個類只能被實例化一次,那么我們就可以使用這種模式。單例模式是相對來講比較簡單的一種設計模式,雖說簡單,卻是處處暗藏殺機。首先我們來看下一個類如何才能只被實例化一次。我們知道一般實例化對象時,都會調用類的構造方法。如果構造方法為public,那么肯定每個人都能實例化這個類,也就無法保證該類對象的唯一性。所以這個類的構造方法必須為private,不能向外界提供。但是這樣我們就無法調用它的構造方法,也就無法實例化對象了,那么由誰來實例化呢?想必你也想到了,由類自身調用,因為這時候也只有它自身能調用構造方法了。我們看下代碼:
public class Singleton {
private Singleton(){}
public Singleton getInsatnce(){
return new Singleton();
}
}
我們在類中定義一個getInstance方法來提供一個該類的實例化對象,不過有個問題,我們該如何調用這個方法呢,因為只有在該類實例化后才能調用這個方法,然而這個方法就是用來實例化對象的。是不是很糾結,有什么好辦法嗎?很簡單我們只要將這個方法表示成靜態方法就可以:
public class Singleton {
private Singleton(){}
public static Singleton getInsatnce(){
return new Singleton();
}
}
這樣我們就可以直接調用Singleton.getInstance()來創建對象了。
惡漢式單例模式
可是這也不是唯一的啊,每次調用這個方法都會新建一個實例對象。別擔心,我們只要稍微改變下就好了:
public class Singleton {
private static Singleton singleton = new Singleton();
private Singleton(){}
public static Singleton getInsatnce(){
return singleton;
}
}
我們在類中事先就創建好一個實例對象,每次調用getInstance方法時返回這個對象就可以了。這樣我們的單例模式就誕生了。我們可以看到,只要這個類一加載完,就會創建實例對象,顯得很著急,像一個惡漢一樣,所以我們稱之為惡漢式單例模式。
懶漢式單例模式
上面的惡漢式單例模式會帶來一個問題,假如我們代碼中都沒用到這個類的實例對象,如果這個類簡單還好,要是復雜的話就會造成大量資源浪費。這時你可以用懶加載的方式來創建對象,即當你要使用這個實例對象時再創建。代碼如下:
public class Singleton {
private static Singleton singleton ;
private Singleton(){}
public static Singleton getInsatnce(){
if (singleton == null){
singleton = new Singleton();
}
return singleton;
}
}
由于只有當用到時才創建對象,比較懶,我們稱之為懶漢式單例模式。
線程安全的懶漢式單例模式(雙重檢驗鎖模式)
以上懶漢式單例模式雖然可以延遲創建實例,不過會帶來新的問題,我們先看個簡單的例子:
我們在上述類的構造方法中加入一句打印語句,如果該類被實例化就會打印出日志。如下:
public class Singleton {
private static Singleton singleton;
private Singleton(){
System.out.print("實例化對象\n");
}
public static Singleton getInsatnce(){
if (singleton == null){
singleton = new Singleton();
}
return singleton;
}
}
然后新建一個測試類來實例化該類:
public class TestClient {
public static void main(String[] args){
Thread thread1 = new Thread(new Runnable() {
@Override
public void run() {
Singleton.getInsatnce();
}
});
Thread thread2 = new Thread(new Runnable() {
@Override
public void run() {
Singleton.getInsatnce();
}
});
thread1.start();
thread2.start();
}
}
測試類也很簡單,創建了兩個線程,每個線程都調用Singleton的getInstance方法來獲取對象。好了我們運行該測試類看下:
輸出結果:
-------------------------------------------
實例化對象
-------------------------------------------
正如我們所愿,雖然我們調用兩次getInstance,但該類只被實例化一次。但不要高興的太早,你多運行幾次后會發現,有的時候會打印兩遍,也就說該類被實例化了兩遍:
輸出結果:
-------------------------------------------
實例化對象
實例化對象
-------------------------------------------
這是為什么呢?我們明明已經做了判斷,如果對象為空就創建,不為空就直接返回,怎么還會創建兩遍呢?其實這正是多線程在作怪。
我們知道,程序的運行其實就是CPU在一條條的執行指令,如果是單線程,那么CPU就會依次執行該線程中的指令,但如果是多線程,比如有兩個線程,線程A和線程B。為了讓兩個線程同時執行,那么CPU會執行A線程一段時間然后暫停,去執行B線程的指令,一段時間后再切換到A線程執行。這樣反復切換直到程序結束,由于CPU切換的速度很快,所以讓人感覺兩個線程是同時執行的。那么到底CPU什么時候切換,以及每次切換執行多久呢,其實這都是隨機的,一般來講每個線程被執行到的幾率都差不多,不過你可以提高某個線程的優先級來讓CPU多執行你會兒,但什么時候切換你是無法控制的,只能提高你被執行到的幾率。
好了,回到上面的例子,我們來看下這個問題到底是怎么產生的。為了便于說明我在Singleton類中標記了兩行代碼,這兩行代碼也正是問題的關鍵:
public class Singleton {
private static Singleton singleton;
private Singleton(){
System.out.print("實例化對象\n");
}
public static Singleton getInsatnce(){
if (singleton == null){ //-----------------------------------1
singleton = new Singleton(); //--------------------------- 2
}
return singleton;
}
}
假設現在有兩個線程A和B,首先A線程調用getInstance方法,當執行到語句1時會判斷對象是否為空,由于該類還沒被實例化,所以條件成立,遍進入到花括號中準備執行語句2,正如前面所說線程的切換是隨機,當正準備執行語句2時,線程A突然停在這里了,CPU切換到線程B去執行。當線程B執行這個方法時,也會判斷語句1的條件是否成立,由于A線程停在了語句1和2之間,實例還未創建,所以條件成立,也會進入到花括號中,注意此時線程B并未停止,而是順利的執行語句2,創建了一個實例,并返回。然后線程B又切換回了線程A,別忘了,這時,線程A還停在語句1和2之間,切換回它的時候就又繼續執行下面的代碼,也就是執行語句2,創建了一個實例,并返回。這樣,兩個對象就被創建出來了,我們的單例模式也就失效了。
好了,找到原因了,那有什么方法解決嗎?很簡單只要在getInstance方法前加上關鍵字synchronized
就可以了。代碼如下:
public class Singleton {
private static Singleton singleton;
private Singleton(){
System.out.print("實例化對象\n");
}
public static synchronized Singleton getInsatnce(){
if (singleton == null){ //-----------------------------------1
singleton = new Singleton(); //--------------------------- 2
}
return singleton;
}
}
用synchronized
修飾這個方法,相當于給這個方法加了把鎖,只要有線程進入到這個方法里面,那么這個鎖就會被鎖上,這時其他的線程想要執行這個方法時,一看,呦,廁所門關著待會再來上。只有當里面的線程執行完這個方法后,這個鎖才會打開,其他線程才能進入。這樣就很好的避免前面重復創建對象的問題。synchronized雖然解決了這個問題,但是synchronized會降低程序執行效率,試想你開車到某地有兩條路,突然其中一條在維修,被封鎖了,那就勢必會造成另一條路的擁堵。看來我們還得再優化下上面的代碼。
由上面的分析我們知道,這個實例的重復創建問題主要是在實例還未被創建的時候,且是在執行語句1,2時產生的,只要實例創建成功了,就沒有必要加鎖了。換句話說,我們沒有必要給整個getInstance方法加鎖,其實只用在實例還未創建時給語句1和語句2加個鎖就可以了,當實例創建成功后會直接返回實例。優化后的代碼如下:
public class Singleton {
private static Singleton singleton;
private Singleton() {
System.out.print("實例化對象\n");
}
public static Singleton getInsatnce() {
if (singleton == null) {
synchronized (Singleton.class) {
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
這里synchronized 的用法和上面的有所不同,上面我們用synchronized 來修飾方法,表示給整個方法上了把鎖,我們稱之為同步方法。這里我們只給語句1和語句2加了把鎖,我們稱這種結構為同步代碼塊,同步代碼塊synchronized 后面的括號中需要一個對象,可以任意,這里我們用了Singleton的類對象Singleton.class
。可以看到我們在方法中進行了兩次對象是否為空的判斷,一次在同步代碼塊外面,一次在里面。因此稱之為雙重檢驗鎖模式(Double Checked Locking Pattern)
。為什么要判斷兩次呢,當還未實例化的時候,就進行同步創建對象,為什么同步代碼塊里面還要做次判斷呢?我們來分析下如果里面不做判斷會怎么樣:
public class Singleton {
private static Singleton singleton;
private Singleton() {
System.out.print("實例化對象\n");
}
public static Singleton getInsatnce() {
if (singleton == null) {//-----------------------------1
synchronized (Singleton.class) {//-----------2
singleton = new Singleton(); //----------3
}
}
return singleton;
}
}
如上,我們在同步代碼塊中去掉了判斷語句,這時有兩個線程A、B調用getInstance方法。假設A先調用,當A調用方法時,會執行語句1進行條件判斷,由于對象尚未創建,所以條件成立,正準備執行語句2來獲取同步鎖。我們上面也分析過了,線程的切換是隨機的,還未執行語句2時,線程A突然停這了,切換到線程B執行。當線程B調用getInstance方法時也會執行語句1進行條件的判斷,由于這時實例還未創建,所以條件成立,注意這時線程B還是沒有停,又繼續執行了語句2和3,即獲取了同步鎖并創建了Singleton對象。這時線程B切換回A,由于A此時還停在語句1和2之間,切回A時,就又繼續執行語句2和3,即獲取同步鎖并創建了Singleton對象,這樣兩個對象就被創建出來了,synchronized 也失去了意義。所以我們需要在同步代碼塊中再做次判斷:
public class Singleton {
private static Singleton singleton;
private Singleton() {
System.out.print("實例化對象\n");
}
public static Singleton getInsatnce() {
if (singleton == null) {//-----------------------------1
synchronized (Singleton.class) {//------------2
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
這樣當線程A從語句1和2之間醒來,然后獲取到同步鎖后在創建對象前做一個判斷,如果對象為空就創建,如果不為空就直接跳出同步代碼塊并返回之前線程B創建的對象。這樣當下次再調用getInstance方法時,由于之前創建過對象,就不會再進入到同步代碼塊中,而是直接返回實例。我們代碼的執行效率也就上去了。好了現在我們的雙重檢驗鎖模式
既解決了在多線程中重復創建對象問題,又提高了代碼執行效率,同時還是懶加載模式,是不是已經非常完美了?別高興的太早,其實這還是有問題的。納尼!!搞了這么久怎么還有問題,有的朋友可能已經坐不住準備退票了。你先別急,讓我們看看到底哪里還有問題。其實問題就出在Singleton的創建語句上:
singleton = new Singleton();
為什么這句會有問題呢,在分析原因之前,我們先來了解下Java虛擬機在執行該對象創建語句時具體做了哪些事情,我們簡單概括為3步:
- 1 在棧內存中創建singleton 變量,在堆內存中開辟一個空間用來存放Singleton實例對象,該空間會得到一個隨機地址,假設為0x0001。
- 2 對Singleton實例對象初始化。
- 3 將singleton變量指向該對象,也就是將該對象的地址0x0001賦值給singleton變量,此時singleton就不為null了。
我們之前說過,程序的運行其實就是CPU在一條條執行指令,有的時候CPU為了提高程序運行效率會打亂部分指令的執行順序,也就是所謂的指令重排序,當然這種指令重排序并不改變最后的運行結果。我們上面的3步就包含了大量的CPU指令,當CPU執行時,是無法保證一定是按照123的順序執行,也可能由于指令重排序的優化,會以132的順序執行。假設現在有兩個線程A、B,CPU先切換到線程A,當執行上述創建對象語句時,假設是以132的順序執行,當線程A執行完3時(執行完第3步后singleton就不為null了),突然停住了,CPU切換到了線程B去調用getInstance方法,由于singleton此時不為null,就直接返回了singleton,但此時步驟2是還沒執行的,返回的對象還是未初始化的,這樣程序也就出問題了。那有什么方法解決嗎?很簡單只要用volatile
修飾singleton變量就可以了:
private volatile static Singleton singleton;
那為什么volatile 修飾變量就可以了呢,我們上面提到指令的重排序,其實CPU在執行指令時并不是無節操隨意打亂順序,而是有一定的原則可尋的,這個原則也叫先行發生原則(happens-before)
,只要不符合這個原則,那么執行順序也是得不到保障的,具體有以下8條原則:
先行發生原則(happens-before):
- 1 程序次序規則:一個線程內,按照代碼順序,書寫在前面的操作先行發生于書寫在后面的操作
- 2 鎖定規則:一個unLock操作先行發生于后面對同一個鎖額lock操作
- 3 volatile變量規則:對一個變量的寫操作先行發生于后面對這個變量的讀操作
- 4 傳遞規則:如果操作A先行發生于操作B,而操作B又先行發生于操作C,則可以得出操作A先行發生于操作C
- 5 線程啟動規則:Thread對象的start()方法先行發生于此線程的每個一個動作
- 6 線程中斷規則:對線程interrupt()方法的調用先行發生于被中斷線程的代碼檢測到中斷事件的發生
- 7 線程終結規則:線程中所有的操作都先行發生于線程的終止檢測,我們可以通過Thread.join()方法結束、Thread.isAlive()的返回值手段檢測到線程已經終止執行
- 8 對象終結規則:一個對象的初始化完成先行發生于他的finalize()方法的開始
這些原則你可能不太理解,不過沒關系,這里我們重點關注第3條:即,對volatile變量的寫操作先行與對這個變量的讀操作。我們知道上面的問題主要是線程B對Singleton 對象讀取時,該對象還未寫入初始化導致的,那么如果我們用volatile來修飾的話,就不會出現這種情況,我們的虛擬機在讀取該對象時,會保證其一定是寫入好的,也就是不會出現132這種情況。這樣也就不會出現問題啦。我們來看下完整的代碼:
public class Singleton {
private volatile static Singleton singleton;
private Singleton() {
System.out.print("實例化對象\n");
}
public static Singleton getInsatnce() {
if (singleton == null) {//-----------------------------1
synchronized (Singleton.class) {//------------2
if (singleton == null) {
singleton = new Singleton();
}
}
}
return singleton;
}
}
好了,這就是完美的雙重檢驗鎖的單例模式啦,放心,現在絕對沒坑了。不過每次寫單例模式都要寫這么多,也是挺麻煩的,有簡單點的嗎?當然有了,下面介紹兩種比較簡潔的單例模式,即用靜態內部類和枚舉來實現,我們來簡單了解下。
靜態內部類實現單例模式
public class Singleton {
private static class SingletonHolder {
private static final Singleton singleton = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.singleton;
}
}
可以看到,我們在Singleton 類內部定義一個SingletonHolder 靜態內部類,里面有一個對象實例,當然由于Java虛擬機自身的特性,只有調用該靜態內部類時才會創建該對象,從而實現了單例的延遲加載,同樣由于虛擬機的特性,該模式是線程安全的,并且后續讀取該單例對象時不會進行同步加鎖的操作,提高了性能。
枚舉實現單例模式
接下來,我們看下如何用枚舉來實現單例。可能有的朋友對枚舉還不是很熟悉,其實枚舉和我們的普通類沒有太大區別,比如我們下面舉個簡單的例子:
public class Person {
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
上面,我們定義了一個簡單的Person類,類中定義了一個屬性name,非常簡單,接下來,如果你想要操作這個類,比如創建一個person對象,并寫入對象的name然后再獲取name,也是非常簡單:
Person person1 = new Person();
person1.setName("張三");
System.out.print(person1.getName());
這里,想必你只要接觸過Java語言都能看懂上面的代碼,我們創建了一個Person對象,并給對象設置了一個名字,然后打印出名字。
接下來我們看下用枚舉如何完成上面的操作,我們把上面的Person類稍加修改:
public enum Person {
person1;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
你可能發現了不同的地方,我們把Person類名前的class改為了enum,表示這個Person類是個枚舉類,然后你會發現,這個類中比之前的類多了一行代碼
person1;
這個person1是什么,你發現前面我們實例化Person的時候也出現過person1,是的你沒猜錯,這里的person1就是我們這個枚舉類的一個對象實例,也就是說,如果你要獲取一個Person對象,不用再像上面那樣調用new Person()來創建對象,直接獲取這個枚舉類中的person1就可以了,這個person1就是一個Person對象實例,你可能不信,沒關系我們來試驗下:
Person.person1.setName("張三");
System.out.print(Person.person1.getName());
運行后你會發現,成功打印出來名字,是不是很方便。可能你會說,那我想再創建一個Person對象比如叫做person2,怎么辦呢,很簡單,只要在person1后面再加上person2
就可以了,如下:
person1,person2;
注意:實例person1和實例person2之間要用逗號隔開,用分號結束,且實例要定義在類中的第一行。
好了,了解的枚舉的簡單實用,問題來了如何將上述的枚舉Person類改為單例呢,很簡單,我們只要在類中中定義一個實例就可以了,比如去掉person2,只保留person1,如下:
public enum Person {
person1;
private String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
這樣你就只能獲取到一個Person實例對象了。可能有的人有疑惑了,不對啊,難道我就不能再new一個嗎?這個是不能的,因為枚舉類的構造方法是私有掉的,你是無法調用到的并且你也無法通過反射來創建該實例,這也是枚舉的獨特之處。可能有人會問了,如果這個Person的name需要在對象創建時就初始化好,那該怎么辦呢?很簡單,就和普通類一樣只要在里面定義一個構造方法,傳入name參數就可以了。如下:
public enum Person {
person1("張三");
private String name;
Person(String name){
this.name = name;
}
public String getName() {
return name;
}
}
可以看到就和普通類一樣,我們在枚舉類中定義了一個入參為name的構造方法,注意這構造方法前面雖然沒有加權限修飾的,但并不表示它的權限是默認的,前面提到枚舉類中的構造方法是私有的,即使你強行給它加個public,編輯器也會報錯。好了,定義好了構造方法,就可以調用。調用也會簡單,直接在實例person1后面加個括號傳入一個名字就可以了,這樣Person中的name字段就有值了,你可以直接調用Person.person1.getName()來獲取這個名字,是不是很方便。
另外枚舉類實例的創建也是線程安全的,所以使用枚舉來實現單例也是一種比較推薦的方法,但是如果你是做Android開發的,你需要注意了,因為使用枚舉所使用的內存是靜態變量的兩倍之上,如果你的應用中使用了大量的枚舉,這將會影響到你應用的性能,不過一般用的不多的話還是沒什么關系的。
好了,單例模式到這里也介紹的差不多,你可以根據自己的喜好以及具體的業務選擇其中一種。
設計模式持續更新中...