JAVA_多線程_單例模式


 

這篇是入職之后的第二篇了,上一篇我簡單介紹了一下LOCK里面的類的方法,感興趣的話可以去了解一下,以后堅持每周至少會更新一篇關於多線程方面的文章,希望博友們可以一起加油成長。

這篇主要的內容是單例模式在多線程環境下的設計,這篇算是比較重要的內容,我會進行文字和代碼的共同說明來講解記錄

1、立即加載(餓漢模式)

說到標題,有人會說什么是立即加載呢?立即加載就是使用類的時候已經將對象創建完畢了,比如說直接new實例化對象。也就是在調用方法之前,實例已經被創建了

  

public class MyObject {
    private static MyObject myObject = new MyObject();
    private MyObject(){

    }
    public static MyObject getInstance(){
        return myObject;
    }
}

 

看這段代碼,他的缺點是不能有其他實例變量。外部的使用者需要使用MyObject實例的時候,只能通過getInstance方法,另外假如沒有用到這個實例的時候,他已經創建了出現,會有資源浪費的情況出現的。還有因為getInstance方法沒有同步,所以有可能出現非線程安全的問題。

2、延遲加載(懶漢模式)

延遲加載就是在調用要使用的那個方法(假如MyObject方法)的時候實例才會被創建,實現方法就是在MyObject方法里面進行new實例化。

  

public class MyObject {
    private static MyObject myObject;
    private MyObject(){

    }
    public static MyObject getInstance(){
        if(myObject==null){
            myObject = new MyObject();
        }
        return myObject;
    }
}

 

此代碼雖然取得了一個對象,沒毛病。單例!但是如果在多線程情況下,就會取出多個實例的情況,這個是與單例模式的初衷背道而馳的。

  

public class MyObject {
    private static MyObject myObject;
    private MyObject(){

    }
    public static MyObject getInstance(){
        if(myObject==null){
            try {
                Thread.sleep(3000);
                myObject = new MyObject();
            }catch(Exception e){
                e.printStackTrace();
            }
        }
        return myObject;
    }
}

可以試着自己多建立幾個線程,運行一下這段代碼,發現在多線程環境在建立出來了多個實例(可以打印對象的hashcode值進行比較)

那我們應該怎么去解決這個問題呢?

3、延遲加載(懶漢模式)——synchronized

  

public class MyObject {
    private static MyObject myObject;
    private MyObject(){

    }
   synchronized public static MyObject getInstance(){
        if(myObject==null){
            try {
                Thread.sleep(3000);
                myObject = new MyObject();
            }catch(Exception e){
                e.printStackTrace();
            }
        }
        return myObject;
    }
}

這樣OK?看上去是的,是解決了得到了相同的實例,但是見到了synchronized這個東西,你不得猶豫一下么?加在了整個方法上啊,如果這個方法設計到比如說很多的過程或者運算,下一個線程想要取得對象,不是要等到程序員找到女朋友才行么。也就是要等到上一個線程釋放鎖之后,才可以繼續進行。

有人說,我不加全部,我加部分不行么?

  

   public static MyObject getInstance(){
        synchronized (MyObject.class) {
            if (myObject == null) {
                try {
                    Thread.sleep(3000);
                    myObject = new MyObject();
                } catch (Exception e) {
                    e.printStackTrace();
                }
            }
        }
        return myObject;
    }
}

你仔細看,有啥大的變化么,並沒有吧。因為效率還是一樣的低低低低。每次我調用getIstance的時候是不是還要同步啊,所以太大變化啦

然后機智的我想出了這樣的方法

  

public class MyObject {
    private static MyObject myObject;
    private MyObject(){

    }
   public static MyObject getInstance(){

        if (myObject == null) {
            try {
                Thread.sleep(3000);
                synchronized (MyObject.class) {
                    myObject = new MyObject();
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return myObject;
    }
}

我只加載了需要創建對象的那個關鍵地方,看到了么,這樣效率大大滴提升了。但是,重點來了,我靠,打印出來了兩個不同對象,打印出來的對象hashcode值不一樣了啊,不是一個對象了,因為兩個線程都進入了if語句內,之后沒有在進行判斷,所以創建了兩個對象。我單例什么呢?所以這個方法也pass

那我到底該怎么辦呢?別着急,DCL雙重檢查所機制,不廢話直接看代碼

  

public class MyObject {
    private static MyObject myObject;
    private MyObject(){

    }
   public static MyObject getInstance(){

        if (myObject == null) {
            try {
                Thread.sleep(3000);
                synchronized (MyObject.class) {
                    if(myObject == null) {
                        myObject = new MyObject();
                    }
                }
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
        return myObject;
    }
}

哇,終於有方法可以實現單例模式在多線程環境下的正常工作了,哈哈哈哈哈,但是但是那你們就打錯特錯了。看如下分析

從JVM的角度來說,怎么創建一個對象呢?第一是申請一塊內存,調用構造方法進行初始化操作,第二是分配一個指針指向這塊內存。這兩個操作誰在前面,誰在后面JVM並不會管它。那么就存在這么一種情況,JVM是先開辟出一塊內存,然后把指針指向這塊內存,最后調用構造方法進行初始化。

線程A開始創建MyObject的實例,此時線程B調用了getInstance()方法,首先判斷MyObject是否為null。假設A已經把MyObject指向了那塊內存,只是還沒有調用構造方法,因此B檢測到MyObject不為null,於是直接把MyObject返回了——問題出現了,盡管MyObject不為null,但它並沒有構造完成結束。此時,如果B在A將MyObject構造完成之前就是用了這個實例,程序就會出現錯誤了!(其實在private static MyObject myObject;  改為   private volatile static MyObject myObject;  就不會發生這樣的結果了。被volatile修飾的寫變量不能和之前的讀寫代碼調整,這里我們當做這個關鍵字不存在,以后會有專門的篇幅去詳細講解這個關鍵字的,這個關鍵字的坑有許多,我們慢慢踩)

那我們到底的咋整啊?

 

public class MyObject {
    private static MyObject myObject;
    private MyObject(){

    }
   public static MyObject getInstance(){

        if (myObject == null) {
            MyObject my;
            synchronized (MyObject.class) {
                my = myObject;
                if (my == null) {
                    synchronized (MyObject.class) {
                        if (my == null) {
                            my = new MyObject();
                            }
                        }
                        myObject = my;
                    }
                }
            }
            return myObject;
        }
    }

  

我們在第一個同步塊里面創建一個臨時變量,然后使用這個臨時變量進行對象的創建,並且在最后把myObject指針臨時變量的內存空間。寫出這種代碼基於以下思想,即synchronized會起到一個代碼屏蔽的作用,同步塊里面的代碼和外部的代碼沒有聯系。因此,在外部的同步塊里面對臨時變量my進行操作並不影響myObject,所以外部類在myObject=my;之前檢測myObject的時候,結果myObject依然是null。

由於同步塊的釋放保證在此之前——也就是同步塊里面——的操作必須完成,但是並不保證同步塊之后的操作不能因編譯器優化而調換到同步塊結束之前進行。因此,編譯器完全可以把myObject=my;這句移到內部同步塊里面執行。又錯了。

4、內部類實現方式

  

public class MyObject_inner {
    private static class MyObjectHandler{
        private static MyObject_inner myObject_inner = new MyObject_inner();
    }
    private MyObject_inner(){}
    public static MyObject_inner getInstance(){
        return MyObjectHandler.myObject_inner;
    }
}

在這一版本的單例模式實現代碼中,我們使用了Java的靜態內部類。這一技術是被JVM明確說明了的,因此不存在任何二義性。在這段代碼中,因為Myobject_inner沒有static的屬性,因此並不會被初始化。直到調用getInstance()的時候,會首先加載MyObjectHandler類,這個類有一個static的MyObject_inne實例,因此需要調用MyObject_inne的構造方法,然后getInstance()將把這個內部類的myobject_inner返回給使用者。由於這個myobject_inner是static的,因此並不會構造多次。

由於MyObjectHandler是私有靜態內部類,所以不會被其他類知道,同樣,static語義也要求不會有多個實例存在。並且,JSL規范定義,類的構造必須是原子性的,非並發的,因此不需要加同步塊。同樣,由於這個構造是並發的,所以getInstance()也並不需要加同步。

但是這種情況完全是對的么?假如遇到序列化的對象呢?會是什么樣的結果?

 

5、序列化與反序列化的單例模式的實現

靜態內部類可以達到線程安全的問題,但是如果遇到序列化對象的時候,使用默認的方式運行得到的結果還是多例的。

具體是為什么在序列化的時候不是單例的,本人我掌握的不太好,后續會把其中涉及到的知識補充之后,在完善此篇文章。

序列化會通過反射調用無參數的構造方法創建一個新的對象。解決的方式就是在implements Serializable這個方法里面加上這段代碼

  
import java.io.Serializable;

public class MyObject_inner implements Serializable{
    private static final long seriaVersionUID = 8899L;
    private static class MyObjectHandler{
        private static MyObject_inner myObject_inner = new MyObject_inner();
    }
    private MyObject_inner(){}
    public static MyObject_inner getInstance(){
        return MyObjectHandler.myObject_inner;
    }

    private Object readResolve(){
        return MyObjectHandler.myObject_inner;
    }
}

  

6、靜態代碼塊實現單例模式

  

public class MyObject_inner {
    private static MyObject_inner instance = null;
    private MyObject_inner(){}
    static {
        instance = new MyObject_inner();
    }
    public static MyObject_inner getInstance(){
        return instance;
    }
}

  

7、枚舉方法實現單例模式

  

public class MyObject_enum {
    public enum EnumSingleton{
        Instance;
        private MyObject_enum instance;
        EnumSingleton(){
            instance = new MyObject_enum();
        }
        public MyObject_enum getInstance(){
            return instance;
        }
    }
}

獲取資源的方式很簡單,只要 EnumSingleton.INSTANCE.getInstance() 即可獲得所要實例。下面我們來看看單例是如何被保證的:

首先,在枚舉中我們明確了構造方法限制為私有,在我們訪問枚舉實例時會執行構造方法,同時每個枚舉實例都是static final類型的,也就表明只能被實例化一次。在調用構造方法時,我們的單例被實例化。也就是說,因為enum中的實例被保證只會被實例化一次,所以我們的INSTANCE也被保證實例化一次。枚舉也提供了序列化機制。所以單元素的枚舉類型已經成為了實現單例模式的最佳方法

  

這篇文章到此暫時先告一段落,里面有一點設計的序列化與反序列化實現單例模式我以后會在繼續的更新補充進去的。 歡迎各位博友批評指正

  
 

 

  

  

  


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM