再說單例

單例模式

保證客戶端運行期間只含有一個對象,其他類不能夠創(chuàng)建對象,對象由本類創(chuàng)建,提供一個獲取該對象的接口。
這個對象創(chuàng)建的方式:

  • 懶漢式:在需要的時候創(chuàng)建實例
  • 餓漢式:類初始化的時候就創(chuàng)建實例
  • 線程安全:通過加鎖
  • 雙重檢查:提高效率
  • 靜態(tài)內(nèi)部類
  • 枚舉

單例模式常問問題總結:

  1. 構造函數(shù)為private是避免在其他類中可以new出一個對象,破壞為唯一性。因為將構造函數(shù)聲明為了private,則需要本來進行對象的創(chuàng)建,并對外提供一個訪問的接口。同時,也其他類無法創(chuàng)建本類對象,不能通過對象來訪問接口,所以需要將接口類型聲明為static,相應的需要將這個對象的引用也聲明為static,因為static方法不能訪問非static變量。

  2. 餓漢式寫法是安全的,因為類加載是按需加載且加載一次,在初始化的時候會創(chuàng)建一個對象實例給引用,之后通過這個引用訪問的都只可能是這個對象。而懶加載是非線程安全的,所以在多線程環(huán)境下會生成多個對象,破壞了唯一性。為了實現(xiàn)同步,需要synchronized對方法或者代碼塊進行修飾。假定synchronized方法加在static方法上,則會嚴重影響效率,因為這是一個類鎖,當一個線程持有這個類鎖時,其他的線程不能夠訪問此類中的其他static synchronized修飾的代碼。synchronized()括號中可以傳入對象或者class,如果是對象則是對象鎖,class則為類鎖。為了效率,可以進行雙重檢查,當判斷當前引用為null時才進行加鎖同步操作。雙重檢查第二層的條件判斷為了避免這種情況:自己已經(jīng)判斷當前對象為null,正要創(chuàng)建對象的時候已經(jīng)有其他線程new出了實例并釋放了同步鎖,自己進入同步代碼塊后又new出一個對象。

  3. 在加上雙重檢查后可以很大程度保證對象的唯一性,但由于指令重排機制,可能會出現(xiàn)new出不完整對象的情況發(fā)生,所以需要在聲明對象引用時在前加上volatile。volatile的作用是保證這個變量對所有線程的可見性以及禁止指令重排序,在這里主要是指令重排序上。new一個對象可經(jīng)過這三個步驟:1. 給對象分配空間 2. 將對象地址賦值給引用 3. 初始化對象。由于指令重排序,則可能步驟2和3是不確定的。當某個線程正要初始化對象時(注意,此時的對象未初始化,但不為null)被其他線程占用,其他線程會得到一個未初始化的對象,代碼執(zhí)行的結果肯定是會受到影響的,而采用了volatile之后禁止指令重排,讓這個情況得到解決。

  4. 利用餓漢式和懶漢式的結合可以采用靜態(tài)內(nèi)部類的方法,它的原理在于單例的引用在內(nèi)部類中,當調(diào)用訪問接口時會觸發(fā)內(nèi)部類的初始化,而JVM會保證這個內(nèi)部類在多線程的情況下被正確地加載和初始化,最終得到同一個單例對象。使用枚舉類創(chuàng)建單例和這個很像,因為枚舉類在第一次使用的時候會初始化(同樣也只初始化一次),并且默認構造函數(shù)是private類型的,所以用它來創(chuàng)建單例代碼很簡潔。

  5. 在懶漢式中,可以利用反射創(chuàng)建多個對象,首先是獲取這個單例類的class對象,然后獲取私有構造方法,設置私有構造方法的可進入屬性為真,最后調(diào)用newInstance()獲取單例對象。

  6. 和反射一樣,單例的序列化與反序列化會破壞對象的唯一性。暫且看一下代碼:

    public static void main(String[] args) {

        ObjectOutputStream out = null;
        try {
            out = new ObjectOutputStream(new FileOutputStream("Singleton"));
            out.writeObject(Singleton.getInstance());

            File file = new File("Singleton");
            ObjectInputStream in = new ObjectInputStream(new FileInputStream(file));
            Singleton singleton = (Singleton) in.readObject();

            System.out.println(singleton == Singleton.getInstance());

        } catch (Exception e) {
            e.printStackTrace();
        }

    }

運行結果是false,說明反序列后的對象確實不一樣,其原理為當ObjectInputStream調(diào)用readObject()方法后,其調(diào)用棧為:

readObject的調(diào)用棧.png

參考自單例與序列化的那些事兒

在方法調(diào)用的過程中,如果反序列化的這個類在運行時能被實例化,則會利用反射調(diào)用無參構造函數(shù)生成實例,否則返回null;接著,方法會判斷這個反序列化的類中是否有readResolve()方法,有的話則通過反射調(diào)用這個方法生成反序列化對象。所以,為了防止反序列化破壞單例,我們可以在單例類中添加如下方法:

private Object readResolve() {
  // 對象生成過程
return 單例對象;
}
?著作權歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務。

相關閱讀更多精彩內(nèi)容

  • 覺得自己現(xiàn)在的生活過的不爽?那你一定沒有讀書,一定也沒有跑步。 要么讀書,要么跑步 讓靈魂和身體得到修煉 明知跑步...
    Ageuvs閱讀 369評論 1 1
  • 早上睡得還不錯、只不過夜里又醒了一次、拉肚子了、 今天一天都窩在家里、把閃電俠二季看了不少、主人公巴里—flash...
    淮鎮(zhèn)閱讀 160評論 0 1
  • 麥田里的守護者,日復一日,兢兢業(yè)業(yè)的守候在那片金黃色的花海里,春耕秋收,從不曾離開他心愛的麥子; 學校里...
    韓寶億閱讀 986評論 4 8

友情鏈接更多精彩內(nèi)容