單例模式(含多線程處理)
單例,顧名思義一個類只有一個實例。為什么要使用單例模式,或者說什么樣的類可以做成單例的?在工作中我發(fā)現(xiàn),使用單例模式的類都有一個共同點,那就是這個類沒有狀態(tài),也就是說無論你實例化多少個對象,其實都是一樣的。又或者是一個類需要頻繁實例化然后銷毀對象。還有很重要的一點,如果這個類有多個實例的話,會產(chǎn)生程序錯誤或者不符合業(yè)務(wù)邏輯。這種情況下,如果我們不把類做成單例,程序中就會存在多個一模一樣的實例,這樣會造成內(nèi)存資源的浪費,而且容易產(chǎn)生程序錯誤。總結(jié)一下,判斷一個類是否要做成單例,最簡單的一點就是,如果這個類有多個實例會產(chǎn)生錯誤,或者在整個應(yīng)用程序中,共享一份資源。
在實際開發(fā)中,一些資源管理器、數(shù)據(jù)庫連接等常常設(shè)計成單例模式,避免實例重復(fù)創(chuàng)建。實現(xiàn)單例有幾種常用的方式,下面我們來探討一下他們各自的優(yōu)劣。
第一種方式:懶漢式單例
public class Singleton {//一個靜態(tài)實例private static Singleton singleton;//私有構(gòu)造方法private Singleton(){}//提供一個公共靜態(tài)方法來獲取一個實例public static Singleton getInstance(){if(singleton == null ){singleton = new Singleton();}return singleton;}}
在不考慮并發(fā)的情況下,這是標準的單例構(gòu)造方式,它通過以下幾個要點來保證我們獲得的實例是單一的。
1、靜態(tài)實例,靜態(tài)的屬性在內(nèi)存中是唯一的;
2、私有的構(gòu)造方法,這就保證了不能人為的去調(diào)用構(gòu)造方法來生成一個實例;
3、提供公共的靜態(tài)方法來返回一個實例, 把這個方法設(shè)置為靜態(tài)的是有原因的,因為這樣我們可以通過類名來直接調(diào)用此方法(此時我們還沒有獲得實例,無法通過實例來調(diào)用方法),而非靜態(tài)的方法必須通過實例來調(diào)用,因此這里我們要把它聲明為靜態(tài)的方法通過類名來調(diào)用;
4、判斷只有持有的靜態(tài)實例為null時才通過構(gòu)造方法產(chǎn)生一個實例,否則直接返回。
在多線程環(huán)境下,這種方式是不安全,通過自己的測試,多個線程同時訪問它可能生成不止一個實例,我們通過程序來驗證這個問題:
public class Singleton {//一個靜態(tài)實例private static Singleton singleton;//私有構(gòu)造方法private Singleton(){}//提供一個公共靜態(tài)方法來獲取一個實例public static Singleton getInstance(){if(singleton == null ){try {Thread.sleep(5000); //模擬線程在這里發(fā)生阻塞} catch (InterruptedException e) {e.printStackTrace();}singleton = new Singleton();}return singleton;}}
測試類:
public class TestSingleton {public static void main(String[] args) {Thread t1 = new MyThread();Thread t2 = new MyThread();t1.start();t2.start();}}class MyThread extends Thread{@Overridepublic void run() {System.out.println(Singleton.getInstance()); //打印生成的實例,會輸出實例的類名+哈希碼值}}
執(zhí)行該測試類,輸出的結(jié)果如下:

從以上結(jié)果可以看出,輸出兩個實例并且實例的hashcode值不相同,證明了我們獲得了兩個不一樣的實例。這是什么原因呢?我們生成了兩個線程同時訪問getInstance()方法,在程序中我讓線程睡眠了5秒,是為了模擬線程在此處發(fā)生阻塞,當?shù)谝粋€線程t1進入getInstance()方法,判斷完singleton為null,接著進入if語句準備創(chuàng)建實例,同時在t1創(chuàng)建實例之前,另一個線程t2也進入getInstance()方法,此時判斷singleton也為null,因此線程t2也會進入if語句準備創(chuàng)建實例,這樣問題就來了,有兩個線程都進入了if語句創(chuàng)建實例,這樣就產(chǎn)生了兩個實例。
為了避免這個問題,在多線程情況下我們要考慮線程同步問題了,最簡單的方式當然是下面這種方式,直接讓整個方法同步:
public class Singleton {//一個靜態(tài)實例private static Singleton singleton;//私有構(gòu)造方法private Singleton(){}//提供一個公共靜態(tài)方法來獲取一個實例public static synchronized Singleton getInstance(){if(singleton == null ){try {Thread.sleep(5000); //模擬線程在這里發(fā)生阻塞} catch (InterruptedException e) {e.printStackTrace();}singleton = new Singleton();}return singleton;}}
我們通過給getInstance()方法加synchronized關(guān)鍵字來讓整個方法同步,我們同樣可以執(zhí)行上面給出的測試類來進行測試,打印結(jié)果如下:

從測試結(jié)果可以看出,兩次調(diào)用getInstance()方法返回的是同一個實例,這就達到了我們單例的目的。這種方式雖然解決了多線程同步問題,但是并不推薦采用這種設(shè)計,因為沒有必要對整個方法進行同步,這樣會大大增加線程等待的時間,降低程序的性能。我們需要對這種設(shè)計進行優(yōu)化,這就是我們下面要討論的第二種實現(xiàn)方式。
第二種方式:雙重校驗鎖
由于對整個方法加鎖的設(shè)計效率太低,我們對這種方式進行優(yōu)化:
public class Singleton {//一個靜態(tài)實例private static Singleton singleton;//私有構(gòu)造方法private Singleton(){}//提供一個公共靜態(tài)方法來獲取一個實例public static Singleton getInstance(){if(singleton == null ){synchronized(Singleton.class){if(singleton == null){singleton = new Singleton();}}}return singleton;}}
跟上面那種糟糕的設(shè)計相比,這種方式就好太多了。因為這里只有當singleton為null時才進行同步,當實例已經(jīng)存在時直接返回,這樣就節(jié)省了無謂的等待時間,提高了效率。注意在同步塊中,我們再次判斷了singleton是否為空,下面解釋下為什么要這么做。假設(shè)我們?nèi)サ暨@個判斷條件,有這樣一種情況,當兩個線程同時進入if語句,第一個線程t1獲得線程鎖執(zhí)行實例創(chuàng)建語句并返回一個實例,接著第二個線程t2獲得線程鎖,如果這里沒有實例是否為空的判斷條件,t2也會執(zhí)行下面的語句返回另一個實例,這樣就產(chǎn)生了多個實例。因此這里必須要判斷實例是否為空,如果已經(jīng)存在就直接返回,不會再去創(chuàng)建實例了。這種方式既保證了線程安全,也改善了程序的執(zhí)行效率。
第三種方式:靜態(tài)內(nèi)部類
public class Singleton {//靜態(tài)內(nèi)部類private static class SingletonHolder{private static Singleton singleton = new Singleton();}//私有構(gòu)造方法private Singleton(){}//提供一個公共靜態(tài)方法來獲取一個實例public static Singleton getInstance(){return SingletonHolder.singleton;}}
這種方式利用了JVM的類加載機制,保證了多線程環(huán)境下只會生成一個實例。當某個線程訪問getInstance()方法時,執(zhí)行語句訪問內(nèi)部類SingletonHolder的靜態(tài)屬性singleton,這也就是說當前類主動使用了改靜態(tài)屬性,JVM會加載內(nèi)部類并初始化內(nèi)部類的靜態(tài)屬性singleton,在這個初始化過程中,其他的線程是無法訪問該靜態(tài)變量的,這是JVM內(nèi)部幫我們做的同步,我們無須擔心多線程問題,并且這個靜態(tài)屬性只會初始化一次,因此singleton是單例的。
第四種方式:餓漢式
public class Singleton {//一個靜態(tài)實例private static Singleton singleton = new Singleton();//私有構(gòu)造方法private Singleton(){}//提供一個公共靜態(tài)方法來獲取一個實例public static Singleton getInstance(){return singleton;}}
這種方式也是利用了JVM的類加載機制,在單例類被加載時就初始化一個靜態(tài)實例,因此這種方式也是線程安全的。這種方式存在的問題就是,一旦Singleton類被加載就會產(chǎn)生一個靜態(tài)實例,而類被加載的原因有很多種,事實上我們可能從始至終都沒有使用這個實例,這樣會造成內(nèi)存的浪費。在實際開發(fā)中,這個問題影響不大。
以上內(nèi)容介紹了幾種常見的單例模式的實現(xiàn)方式,分析了在多線程情況下的處理方式, 在工作中可根據(jù)實際需要選擇合適的實現(xiàn)方式。還有一種利用枚舉來實現(xiàn)單例的方式,在工作中很少有人這樣寫過,不做探討。

騰訊、阿里、滴滴后臺面試題匯總總結(jié) — (含答案)
面試:史上最全多線程面試題 !
最新阿里內(nèi)推Java后端面試題
JVM難學?那是因為你沒認真看完這篇文章

關(guān)注作者微信公眾號 —《JAVA爛豬皮》
了解更多java后端架構(gòu)知識以及最新面試寶典


看完本文記得給作者點贊+在看哦~~~大家的支持,是作者源源不斷出文的動力
作者:風中程序猿
出處:https://www.cnblogs.com/fangfuhai/p/6666850.html
