你真的理解了java單例模式嗎?講別人都忽略的細節!

努力的老劉發表於2020-12-23

前言:老劉這篇文章敢做保證,java的單例模式講的比大多數的技術部落格都要好,講述別人技術部落格都沒有的細節!!!

1 java單例模式

直接講實現單例模式的兩種方法:懶漢式和餓漢式,單例模式的概念自己上網搜吧這裡就不講了!

這裡會涉及到java中的jvm,如果你沒有這方面的知識,我建議你先去補補,不然會有點迷糊!

首先說說類什麼時候進行載入?

java虛擬機器沒有進行強制性的約束,但是對於初始化卻嚴格規定了有且只有4種情況必須先對類進行初始化。

我們要知道的是在類載入的過程中,載入、驗證、準備是在初始化之前完成的,所以進行了初始化,載入、驗證、準備自然就在之前完成了。

然後這四種情況是分別遇到 new 、 getstatic 、 putstatic 和 invokestatic 這四條指令時,如果對應的類沒有初始化,則要對對應的類先進行初始化。

講完類載入時機,就可以講懶漢式和餓漢式了。

直接先說說懶漢式為什麼是執行緒不安全的?

先看最開始的程式碼:

public class Student2 {

    //1:構造私有
    private Student2(){}
    //2:定義私有靜態成員變數,先不初始化
    private static Student2 student = null;

    //3:定義公開靜態方法,獲取本身物件
    public static Student2 getSingletonInstance(){
        //沒有物件,再去建立
        if (student == null) {
            student = new Student2();
        }
        //有物件就返回已有物件
        return student;
    }   
}

結合之前講的類載入內容,遇到new或載入靜態方法了就會進行類載入了。

執行緒1它new了一個物件,執行緒2它緊接著也new一個物件,第二個物件的值把第一個物件的值覆蓋了,不管new了多少個物件,都會產生垃圾物件,只有最後一個物件才會保持住,其他物件都會變成不可達物件,被垃圾回收,這個過程就相當於產生了大量無效物件,這就是執行緒不安全的原因!

那為了讓懶漢式變得執行緒安全,我們要怎麼做?

看程式碼:

public class Student4 {

    private volatile static Student4 student = null;
    private Student4() {}

    public static Student4 getSingletonInstance() {
        if (student == null) {//第一個null判斷,是先大範圍過濾一遍
            synchronized (Student4.class) {
                if (student == null) {
                    student = new Student4();
                }
            }
        }
        return student;
    }
}

這個叫雙重檢查鎖DCL,第一個if先大範圍判斷是不是空值,經過synchronized,執行緒1先進去執行完後,執行緒2才能進去,然後第二個if判斷是否完成建立類的例項,執行緒1建立完了,執行緒2就不用建立了。

那為什麼要加volatile關鍵字呢?

因為我們Student student = new Student()的執行過程是:

1、new觸發類載入機制(已經被載入過的類不需要再次載入)

2、分配記憶體空間

3、將物件進行初始化4、講物件引用地址賦值給棧空間中的變數但我們JVM中的JIT即時編輯器會對程式碼的執行過程進行優化,把過程變為1、2、4、3。

這是什麼意思呢?就是未經初始化直接賦值,這樣就是student直接有值了,但整個物件還未初始化完成,所以這個物件是不完整的,是個未成品。在JVM規範中,它是一個根本不能用的物件。

到了這個時候,執行緒1做了這麼多事,我們讓它休息會,給CPU稍微停一下,執行緒2就來了,它就直接得到了物件,但它呼叫物件的方法時,就會報錯。雖然這個物件有值,但還未初始化完成。所以我們要加上volatile關鍵字禁止指令重新排序。

面試重災區說的差不多了,餓漢式還是要講講。

最後就說說餓漢式為什麼沒有執行緒安全問題?​

看程式碼:

public class Student1 {
    // 2:成員變數初始化本身物件
    private static Student1 student = new Student1();
    // 構造私有
    private Student1() {
    }
    // 3:對外提供公共方法獲取物件
    public static Student1 getSingletonInstance() {
        return student;
    }
    public void sayHello(String name) {
        System.out.println("hello," + name);
    }
}

根據類載入的東西,在多執行緒的條件下,執行緒1先執行getSingletonInstance()時,就會進行類載入,類的靜態資源就會進行初始化。根據JVM安全機制裡說的,當一個類被JVM載入的時候,該類的載入是執行緒安全的,相當於JVM對該過程加鎖了。所以整個過程處於一個鎖的範圍內,然後靜態成員變數進行初始化就相當於Student1()被new了,只會被new一次。

當第二個執行緒進來,它就發現這個類已經被載入了,就不需要進行載入了,物件也不需要頻繁建立,所以執行緒是安全的!

2 總結

老劉看過很多關於java單例模式的資料,多多少少都會缺少一點細節,這次老劉把它補全了。

最後,如果覺得有哪裡寫的不好或者有錯誤的地方,可以聯絡公眾號:努力的老劉,進行交流。

如果覺得寫的不錯,給老劉點個贊!

 

相關文章