詳細說說單例模式

單例模式

特點:全局唯一,在整個程序中,只有一個對象。

什麼樣的類適合單例?

  • 全局使用的類
  • 創建和銷燬會消耗很多系統資源的類
    • 數據庫連接池
    • 工廠類
    • 數據源

應用:

  • Spring的Bean默認情況下是單例
  • 項目中,讀取配置文件的類,一般也只有一個對象。沒有必要每次使用配置文件數據,每次new一個對象去讀取。
  • 應用程序的日誌應用,一般都何用單例模式實現,這一般是由於共享的日誌文件一直處於打開狀態,因爲只能有一個實例去操作,否則內容不好追加。
  • 數據庫連接池的設計一般也是採用單例模式,因爲數據庫連接是一種數據庫資源。
  • 操作系統的文件系統,也是大的單例模式實現的具體例子,一個操作系統只能有一個文件系統。
  • Application 也是單例的典型應用(Servlet編程中會涉及到)
  • 在servlet編程中,每個Servlet也是單例
  • 在spring MVC框架/struts1框架中,控制器對象也是單例

餓漢式

優點:

  • 類一加載就創建實例,沒有延遲
  • 線程安全

缺點:

  • 反射和反序列化會破壞單例
  • 如果沒有用到該類就會產生內存浪費
  • 如果加載的資源很大,程序啓動的時候就會產生效率問題
class Singleton implements Serializable {
    private Singleton() {
    }

    private static final Singleton INSTANCE = new Singleton();

    public static Singleton getInstance() {
        return INSTANCE;
    }
}

反射破壞單例:

@Test
public void test() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
    Singleton instance = Singleton.getInstance();
    Class<Singleton> clazz = Singleton.class;
    Constructor<Singleton> constructor = clazz.getDeclaredConstructor();
    constructor.setAccessible(true);
    Singleton instance1 = constructor.newInstance();
    System.out.println(instance == instance1); // false
}

反序列化破壞單例:

@Test
public void test() throws IOException, ClassNotFoundException {
    Singleton i1 = Singleton.getInstance();
    ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("./singleton.obj"));
    out.writeObject(i1);
    out.close();

    FileInputStream in = new FileInputStream("./singleton.obj");
    ObjectInputStream ois = new ObjectInputStream(in);
    Singleton i2 = (Singleton) ois.readObject();
    System.out.println(i1);
    System.out.println(i2);
    // com.qianyu.thread.Singleton@6842775d
    // com.qianyu.thread.Singleton@1ae369b7
}

加入readResolve方法解決反序列化問題

import java.io.*;

class Singleton implements Serializable {
    private Singleton() {
    }

    private static final Singleton INSTANCE = new Singleton();

    public static Singleton getInstance() {
        return INSTANCE;
    }

    public Object readResolve() {
        return INSTANCE;
    }
}

爲了防止反射破壞單例,我們可以在構造器中添加判斷

import java.io.*;

class Singleton implements Serializable {
    private Singleton() {
        if (INSTANCE != null) {
            throw new RuntimeException("不能通過反射創建對象");
        }
    }

    private static final Singleton INSTANCE = new Singleton();

    public static Singleton getInstance() {
        return INSTANCE;
    }
}

懶漢式

特點:

  • 延時加載
  • 線程不安全
public class Singleton {
    private static Singleton instance;

    private Singleton() {
    }

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

模擬線程不安全情況:

@Test
public void test() throws InterruptedException {
    for (int i = 0; i < 20; i++) {
        new Thread(() -> System.out.println(Singleton.getInstance())).start();
    }
    Thread.sleep(1000);
}

使用同步解決線程不安全問題

public class Singleton {
    private static Singleton instance;

    private Singleton() {
    }

    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}
public class Singleton {
    private static Singleton instance;

    private Singleton() {
    }

    public static Singleton getInstance() {
        synchronized (Singleton.class) {
            if (instance == null) {
                instance = new Singleton();
            }
        }
        return instance;
    }
}

雙重檢測鎖模式

懶漢式的單例模式雖然可以使用synchronized解決線程不安全問題,但是每次獲取單例對象的時候都要執行synchronized代碼塊,造成效率低,爲了解決這個問題,提出雙重檢測鎖模式

public class Singleton {
    private static Singleton instance;

    private Singleton() {
    }

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

指令重排問題

instance = new Singleton()會執行如下操作:

  1. 分配內存空間
  2. 初始化對象
  3. instance執行(1)中分配的空間

但是在某些編譯器上,可能會發生指令重排:

  1. 分配內存空間
  2. instance執行(1)中分配的空間(但此時對象沒有初始化)
  3. 初始化對象

這樣的話,如果多個線程同時訪問的話,有可能會出現某些對象爲空的情況。爲了防止JVM進行指令重排,我們可以加上volatile關鍵字

public class Singleton {
    private static volatile Singleton instance;

    private Singleton() {
    }

    public static Singleton getInstance() {
        if (instance == null) {
            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }
        }
        return instance;
    }
}

登記式

登記式,也可以叫做“靜態內部類”式。

首先我們要知道:

  • 只有調用內部類的時候,內部類纔開始加載(延時加載)
  • 非靜態內部類要依附於外部類,也就是說外部類必須創建對象才能使用非靜態內部類
  • 靜態內部類不用外部類創建對象就可以使用
class Test {
    class Inner {
    }

    static class SInner {
    }
}

public class Demo {
    public static void main(String[] args) {
        Test.Inner inner = new Test().new Inner();
        Test.SInner s = new Test.SInner();
    }
}

登記式相對於餓漢式的好處:

  • 可以實現延遲加載
  • 可以通過改造,使其對反射安全
public class Singleton {
    private static class SingletonHolder {
        private static Singleton instance = new Singleton();
    }

    // 如果使用反射調用會報錯
    private Singleton() {
        if (SingletonHolder.instance != null) {
            throw new IllegalStateException();
        }
    }

    public Singleton getInstance() {
        return SingletonHolder.instance;
    }
}

枚舉式

特點:

  • java1.5之後出現
  • 目前推薦實現單例的最佳方式
  • 線程安全
  • 立即初始化
  • 自動支持序列化,防止反序列化創建新的對象
  • 防止反射攻擊
public enum Singleton {
    INSTANCE{
        @Override
        protected void dosomething() {
            System.out.println("Singleton.dosomething");
        }
    };

    protected abstract void dosomething();
}

反射調用構造器,會拋出異常:

@Test
public void test() throws NoSuchMethodException, IllegalAccessException, InvocationTargetException, InstantiationException {
    Class<Singleton> clazz = Singleton.class;
    Constructor<Singleton> constructor = clazz.getDeclaredConstructor();
    constructor.setAccessible(true);
    Singleton s1 = constructor.newInstance();
    // java.lang.NoSuchMethodException: com.qianyu.thread.Singleton.<init>()
    //	 at java.lang.Class.getConstructor0(Class.java:3082)
    //	 at java.lang.Class.getDeclaredConstructor(Class.java:2178)
    //	 at com.qianyu.thread.Application.test(Application.java:11)
}

ThreadLocal

優點:

  • 空間換時間
  • 延時加載

缺點:

  • 只能是在同一個線程中獲得的兩個對象纔是單例

代碼實現:

public class Singleton {
    private static Singleton instance;

    private Singleton() {
    }

    private static ThreadLocal<Singleton> threadLocalSingleton = new ThreadLocal<Singleton>() {
        @Override
        protected Singleton initialValue() {
            return new Singleton();
        }
    };

    public static Singleton getInstance() {
        return threadLocalSingleton.get();
    }
}

多線程環境模擬

@Test
public void test() throws InterruptedException {
    for (int i = 0; i < 20; i++) {
        new Thread(() -> {
            Singleton s1 = Singleton.getInstance();
            Singleton s2 = Singleton.getInstance();
            System.out.println("s2 = " + s2 + " , s1 = " + s1);

        }).start();
    }
    Thread.sleep(1000);
}

CAS

缺點:

  • 可能會產生垃圾對象

代碼實現

import java.util.concurrent.atomic.*;

public class Singleton {
    private static final AtomicReference<Singleton> instance = new AtomicReference<>();

    private Singleton() {
    }

    public static final Singleton getInstance() {
        while (true) {
            Singleton current = instance.get();
            if (current != null) {
                return current;
            }
            current = new Singleton();
            if (instance.compareAndSet(null, current)) {
                return current;
            }
        }
    }
}

總結

單例實現要點:

  1. 私有構造器
  2. 持有該類的屬性
  3. 對外提供獲取實例的靜態方法

上述幾種單例模式的比較

  • 餓漢式:線程安全、反射不安全、反序列化不安全、非延時加載
  • 懶漢式:線程不安全、反射不安全、反序列化不安全、延時加載
  • 雙重檢測鎖:線程安全、反射不安全、反序列化不安全、延時加載
  • 登記時:線程安全、反射安全、反序列化不安全、延時加載
  • 枚舉式:線程安全、反射安全、反序列化安全、非延時加載
  • ThreadLocal:不加鎖,以空間換時間,爲每個線程提供獨立副本,可以保證各自線程是單例的,但是不同線程之間不是單例的
  • CAS:無鎖樂觀策略,線程安全
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章