單例模式--還沒從工廠中逃脫出來?看來是註定單身了..

Melo~發表於2021-10-26

前言

上次我們聊了聊一個略微重量級的工廠模式,不知道你是否消化完從工廠中逃脫出來了呢?不是我說,今天的單例模式,恰恰好相反了,孤孤單單,看來是註定單身了..

先來看看單例模式在jdk中的應用

在jdk中Runtime用到,餓漢式

image.png

知識點

總共8種方式

1)餓漢式(靜態常量)
2)餓漢式(靜態程式碼塊)
3)懶漢式(執行緒不安全)
4)懶漢式(執行緒安全,同步方法)
5)懶漢式(執行緒安全,同步程式碼塊)
6)雙重檢查
7)靜態內部類
8)列舉

餓漢式(靜態常量)

非常勤快,在物件還沒使用到的時候就先建立出來了

1)構造器私有化(防止new)
2)類的內部建立物件
3)向外暴露一個靜態的公共方法 getInstance()
4)程式碼實現

public class Singleton1 {

    private  Singleton1() {
    }

    private final static Singleton1 instance = new Singleton1();

    public static Singleton1 getInstance(){
        return instance;
    }
}

優缺點

1)優點:這種寫法比較簡單,就是在類裝載的時候就完成例項化。避免了執行緒同步問題。
2)缺點:在類裝載的時候就完成例項化,沒有達到LazyLoading的效果。如果從始至終從未使用過這個例項,則會造成記憶體的浪費
3)這種方式基於classloder機制避免了多執行緒的同步問題,不過,instance在類裝載時就例項化,在單例模式中大多數都是呼叫getInstance方法,但是導致類裝載的原因有很多種,因此不能確定有其他的方式(或者其他的靜態方法)導致類裝載,這時候初始化instance就沒有達到lazyloading的效果
4)結論:這種單例模式可用,可能造成記憶體浪費

餓漢式(靜態程式碼塊)

優缺點跟上邊是一樣的,可以說是等效於上邊的普通餓漢式

package com.melo.design.單例模式.餓漢式_靜態程式碼塊;


public class Singleton2 {

    private Singleton2() {
    }

    private static Singleton2 instance ;
    
    static {
        instance = new Singleton2();
    }

    public static Singleton2 getInstance(){
        return instance;
    }
}

懶漢式

等到要用到了,再把物件建立出來

/**
 * 懶漢式
 *  執行緒安全
 */
public class Singleton {
    //私有構造方法
    private Singleton() {}

    //在成員位置建立該類的物件
    private static Singleton instance;

    //對外提供靜態方法獲取該物件
    public static synchronized Singleton getInstance() {

        if(instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

懶漢式(同步程式碼塊)--不能使用

實際上做不到執行緒同步,因為在if的時候就會出現執行緒混亂問題
比如if時A進來,B再進來,A先使用class,然後new了,B後續沒有再次判斷,還是會再次去new

image.png

**懶漢式雙檢索(雙重檢查)

剛好解決了上邊懶漢式同步程式碼塊的問題,再多一步if判斷

  • 注意雙檢索是說雙重檢查,而不是說加了雙重鎖!!!!
package com.melo.design.單例模式.雙檢索;

public class Singleton {
    
    //注意volatile修飾
    private volatile static Singleton singleton;
    
    private Singleton (){
    }
    
    public static Singleton getSingleton() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

**注意: volatile

在多執行緒的情況下,可能會出現空指標問題,出現問題的原因是JVM在例項化物件的時候會進行優化和指令重排序操作。
要解決雙重檢查鎖模式帶來空指標異常的問題,只需要使用 volatile 關鍵字, volatile 關鍵字可以保證可見性和有序性。

  • 那這裡那裡可能會出現指令重排序呢,既然涉及到多條指令會重排序,那麼說明這裡的 **new Singleton() **操作並不是原子性的
    1. 先給singleton分配記憶體空間
    2. 呼叫建構函式來初始化成員變數singleton
    3. 讓singleton引用指向所分配好的記憶體空間(此時instance就!=null了)
  • 那什麼情況下指令重排序後會出現問題呢?
    • 比如說** c - a - b 這種情況下,先走c , 然後此時別的執行緒呼叫了 getSingleton ,注意此時他判斷 singleton不等於null後,就會直接return instance** 了,然而得到的singleton卻是不可用的,因為該物件還沒有初始化,狀態還處於不可用的狀態,這樣會導致異常的發生。
  • 所以我們就得加volatile關鍵字來修飾該變數,便可避免指令重排序

有關volatile相關的知識,目前還在整理,希望可以早點把併發和鎖相關的知識一併整理出來

**靜態內部類

裡邊定義一個final靜態常量
使用時直接返回靜態內部類的final靜態常量就好了!

public class Singleton {  
    
    //靜態內部類,裡邊定義一個final靜態常量
    private static class SingletonHolder {  
    	private static final Singleton INSTANCE = new Singleton();  
    }  
    
    private Singleton (){}  
    
    public static final Singleton getInstance() {  
    	return SingletonHolder.INSTANCE;  
    }  
}

1)這種方式採用了類裝載的機制來保證初始化例項時只有一個執行緒。
2)靜態內部類方式在Singleton類被裝載時並不會立即例項化,而是在需要例項化時,呼叫getInstance方法,才會裝載SingletonInstance類,從而完成Singleton的例項化。
3)類的靜態屬性只會在第一次載入類的時候初始化,所以在這裡,JVM幫助我們保證了執行緒的安全性,在類進行初始化時,別的執行緒是無法進入的。
4)優點:避免了執行緒不安全,利用靜態內部類特點實現延遲載入,效率高
5)結論推薦使用.

**列舉(防反射和反序化)

  • 可以繼承也可以實現介面(似乎跟普通的class沒有什麼區別)
  • 要用直接StuUserService.INSTANCE.方法即可
public enum StuUserService implements StuUserServiceInter {
    /**
     * 該類的唯一例項
     */
    INSTANCE;

    @Override
    public void test1(){
        System.out.println("111");
    }

    public static void main(String[] args) {
        StuUserService.INSTANCE.test1();
    }
}



擴充套件知識

image.png
首先,列舉類似類,一個列舉可以擁有成員變數,成員方法,構造方法。

每一個列舉量看作是這個類的物件
同時可以設定成員變數,成員方法
然後可以根據成員變數設定相應的構造方法

反射和序列化破壞單例

反射

私有化構造器並不保險。它抵禦不了反射的攻擊!!!

破壞一下引以為傲的"雙檢索"

image.png

package com.melo.mydesign.單例模式.雙檢索;

import java.lang.reflect.Constructor;
import java.lang.reflect.InvocationTargetException;

public class Singleton {
    private volatile static Singleton singleton;

    private Singleton (){}

    public static Singleton getSingleton() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
    //開始搞事情
    public static void main(String[] args) throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
        //呼叫單例方法生成的物件1
        Singleton singleton1 = Singleton.getSingleton();
        //獲得所有構造器(包括私有的)
        Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor();
        //設定可訪問
        constructor.setAccessible(true);
        //構造器生成
        Singleton singleton2 = constructor.newInstance();
        //判斷是否相等,發現false,已然破壞了單例
        System.out.println(singleton1==singleton2);
    }
}

序列化

同樣破壞雙檢索

public static void main(String[] args) throws Exception {
        Singleton s = Singleton.getInstance();

        byte[] serialize = SerializationUtils.serialize(s);
        Object deserialize = SerializationUtils.deserialize(serialize);

        System.out.println(s);
        System.out.println(deserialize);
        System.out.println(s == deserialize);

    }

image.png

來看看列舉大法

反射一執行就報錯

Exception in thread "main" java.lang.NoSuchMethodException: com.fsx.bean.EnumSingleton.<init>()
	at java.lang.Class.getConstructor0(Class.java:3082)
	at java.lang.Class.getDeclaredConstructor(Class.java:2178)
	at com.fsx.maintest.Main.main(Main.java:19)

主要是在原始碼處有對列舉特判,如果該類是列舉修飾,則丟擲異常

if ((clazz.getModifiers() & Modifier.ENUM) != 0){
            throw new IllegalArgumentException("Cannot reflectively create enum objects");
}

同樣序列化也是可以防範的

package com.melo.mydesign.單例模式.列舉;


import com.melo.mydesign.單例模式.雙檢索.Singleton;
import org.springframework.util.SerializationUtils;

public enum StuUserService implements StuUserServiceInter {
    /**
     * 該類的唯一例項
     */
    INSTANCE;

    @Override
    public void test1(){
        System.out.println("111");
    }

    public static void main(String[] args) {
//        StuUserService.INSTANCE.test1();

        StuUserService s = StuUserService.INSTANCE;

        byte[] serialize = SerializationUtils.serialize(s);
        Object deserialize = SerializationUtils.deserialize(serialize);

        System.out.println(s);
        System.out.println(deserialize);
        System.out.println(s == deserialize);
    }
}

image.png

總結

  • 一個類有static變數instance,以及私有構造方法,要訪問這個類物件的話有兩種情況

懶漢式: 用到了再來建立物件(就得加鎖,判斷是不是null,是null得加鎖才來生成,防止多次例項化)
餓漢式(比較勤快):類載入時就先生產好了物件,保證了執行緒安全,但浪費了記憶體空間


相關文章