單例(Singleton)

目錄

 

Intent

Class Diagram

Implementation

餓漢式—線程安全

懶漢式—線程不安全

懶漢式—線程安全

雙重校驗鎖—線程安全

靜態內部類實現

枚舉實現


Intent

確保一個類只有一個實例,並提供該實例的全局訪問點。

Class Diagram

使用一個私有構造函數、一個私有靜態變量以及一個公有靜態函數來實現。

私有構造函數保證了不能通過構造函數來創建對象實例,只能通過公有靜態函數返回唯一的私有靜態變量。

Implementation

餓漢式—線程安全

package com.singleton;

/**
 * 餓漢式
 * 類加載到內存後,就實例化一個單例,JVM保證線程安全
 * 簡單實用,推薦使用!
 * 唯一缺點:不管用到與否,類裝載時就完成實例化
 */
public class Mgr01 {
    private static final Mgr01 INSTANCE = new Mgr01();

    private Mgr01() {};

    public static Mgr01 getInstance() {
        return INSTANCE;
    }

    public void m() {
        System.out.println("m");
    }

    public static void main(String[] args) {
        Mgr01 m1 = Mgr01.getInstance();
        Mgr01 m2 = Mgr01.getInstance();
        System.out.println(m1 == m2);
    }
}

懶漢式—線程不安全

以下實現中,私有靜態變量 INSTANCE被延遲實例化,這樣做的好處是,如果沒有用到該類,那麼就不會實例化 INSTANCE,從而節約資源。

這個實現在多線程環境下是不安全的,如果多個線程能夠同時進入if(INSTANCE == null),並且此時 INSTANCEnull,那麼會有多個線程執行INSTANCE = new Mgr03();語句,這將導致實例化多次 INSTANCE

package com.singleton;

/**
 * lazy loading
 * 也稱懶漢式
 * 雖然達到了按需初始化的目的,但卻帶來線程不安全的問題
 */
public class Mgr03 {
    private static Mgr03 INSTANCE;

    private Mgr03() {
    }

    public static Mgr03 getInstance() {
        if (INSTANCE == null) {
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            INSTANCE = new Mgr03();
        }
        return INSTANCE;
    }

    public void m() {
        System.out.println("m");
    }

    public static void main(String[] args) {
        for(int i=0; i<100; i++) {
            new Thread(()->
                System.out.println(Mgr03.getInstance().hashCode())
            ).start();
        }
    }
}

懶漢式—線程安全

只需要對 getInstance() 方法加鎖,那麼在一個時間點只能有一個線程能夠進入該方法,從而避免了實例化多次 INSTANCE

但是當一個線程進入該方法之後,其它試圖進入該方法的線程都必須等待,即使 INSTANCE已經被實例化了。這會讓線程阻塞時間過長,因此該方法有性能問題,不推薦使用。

package com.singleton;

/**
 * lazy loading
 * 也稱懶漢式
 * 雖然達到了按需初始化的目的,但卻帶來線程不安全的問題
 * 可以通過synchronized解決,但也帶來效率下降
 */
public class Mgr04 {
    private static Mgr04 INSTANCE;

    private Mgr04() {
    }

    public static synchronized Mgr04 getInstance() {
        if (INSTANCE == null) {
            try {
                Thread.sleep(1);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            INSTANCE = new Mgr04();
        }
        return INSTANCE;
    }

    public void m() {
        System.out.println("m");
    }

    public static void main(String[] args) {
        for(int i=0; i<100; i++) {
            new Thread(()->{
                System.out.println(Mgr04.getInstance().hashCode());
            }).start();
        }
    }
}

雙重校驗鎖—線程安全

INSTANCE只需要被實例化一次,之後就可以直接使用了。加鎖操作只需要對實例化那部分的代碼進行,只有當 uniqueInstance 沒有被實例化時,才需要進行加鎖。

雙重校驗鎖先判斷 INSTANCE是否已經被實例化,如果沒有被實例化,那麼纔對實例化語句進行加鎖。

package com.singleton;

/**
 * 雙重校驗鎖
 */
public class Mgr06 {
    //volatile 確保本條指令不會因編譯器的優化而省略。
    private static volatile Mgr06 INSTANCE;

    private Mgr06() {
    }

    public static Mgr06 getInstance() {
        if (INSTANCE == null) {
            //雙重檢查
            synchronized (Mgr06.class) {
                if(INSTANCE == null) {
                    try {
                        Thread.sleep(1);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                    INSTANCE = new Mgr06();
                }
            }
        }
        return INSTANCE;
    }

    public void m() {
        System.out.println("m");
    }

    public static void main(String[] args) {
        for(int i=0; i<100; i++) {
            new Thread(()->{
                System.out.println(Mgr06.getInstance().hashCode());
            }).start();
        }
    }
}

考慮下面的實現,也就是隻使用了一個 if 語句。在 INSTANCE == null 的情況下,如果兩個線程都執行了 if 語句,那麼兩個線程都會進入 if 語句塊內。雖然在 if 語句塊內有加鎖操作,但是兩個線程都會執行INSTANCE = new Mgr06();這條語句,只是先後的問題,那麼就會進行兩次實例化。因此必須使用雙重校驗鎖,也就是需要使用兩個 if 語句:第一個 if 語句用來避免 INSTANCE已經被實例化之後的加鎖操作,而第二個 if 語句進行了加鎖,所以只能有一個線程進入,就不會出現INSTANCE == null時兩個線程同時進行實例化操作。

package com.singleton;

/**
 * lazy loading
 * 也稱懶漢式
 * 雖然達到了按需初始化的目的,但卻帶來線程不安全的問題
 */
public class Mgr05 {
    private static Mgr05 INSTANCE;

    private Mgr05() {
    }

    public static Mgr05 getInstance() {
        if (INSTANCE == null) {
            //妄圖通過減小同步代碼塊的方式提高效率,但是線程依然不安全
            // 兩個線程判斷INSTANCE == null後,其中一個線程拿到鎖實例化,釋放鎖。
            // 另外一個線程依然可以進行實例化,需要加雙重校驗
            synchronized (Mgr05.class) {
                try {
                    Thread.sleep(1);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
                INSTANCE = new Mgr05();
            }
        }
        return INSTANCE;
    }

    public void m() {
        System.out.println("m");
    }

    public static void main(String[] args) {
        for(int i=0; i<100; i++) {
            new Thread(()->{
                System.out.println(Mgr05.getInstance().hashCode());
            }).start();
        }
    }
}

INSTANCE 採用 volatile 關鍵字修飾也是很有必要的,INSTANCE = new Mgr05()這段代碼其實是分爲三步執行:

  1. INSTANCE分配內存空間
  2. 初始化 INSTANCE
  3. INSTANCE指向分配的內存地址

但是由於 JVM 具有指令重排的特性,執行順序有可能變成 1>3>2。指令重排在單線程環境下不會出現問題,但是在多線程環境下會導致一個線程獲得還沒有初始化的實例。例如,線程 T1 執行了 1 和 3,此時 T2 調用getInstance() 後發現 INSTANCE不爲空,因此返回 INSTANCE,但此時 INSTANCE還未被初始化。

使用 volatile 可以禁止 JVM 的指令重排,保證在多線程環境下也能正常運行。

靜態內部類實現

Mgr07類被加載時,靜態內部類 Mgr07holder沒有被加載進內存。只有當調用getInstance()方法從而觸發Mgr07holder.INSTANCEMgr07holder纔會被加載,此時初始化 INSTANCE 實例,並且 JVM 能確保 INSTANCE 只被實例化一次。

這種方式不僅具有延遲初始化的好處,而且由 JVM 提供了對線程安全的支持。

package com.singleton;

/**
 * 靜態內部類方式
 * JVM保證單例
 * 加載外部類時不會加載內部類,這樣可以實現懶加載
 */
public class Mgr07 {

    private Mgr07() {
    }

    private static class Mgr07Holder {
        private final static Mgr07 INSTANCE = new Mgr07();
    }

    public static Mgr07 getInstance() {
        return Mgr07Holder.INSTANCE;
    }

    public void m() {
        System.out.println("m");
    }

    public static void main(String[] args) {
        for(int i=0; i<100; i++) {
            new Thread(()->{
                System.out.println(Mgr07.getInstance().hashCode());
            }).start();
        }
    }


}

枚舉實現

package com.singleton;

/**
 * 不僅可以解決線程同步,還可以防止反序列化。
 * 枚舉類沒有構造方法
 */
public enum Mgr08 {

    INSTANCE;

    public void m() {}

    public static void main(String[] args) {
        for(int i=0; i<100; i++) {
            new Thread(()->{
                System.out.println(Mgr08.INSTANCE.hashCode());
            }).start();
        }
    }

}

 

發佈了87 篇原創文章 · 獲贊 18 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章