Java設計模式-單例模式

小薯條學技術發表於2023-04-28

單例模式原理

什麼是單例物件?

有些物件我們只需要一個如執行緒池、快取dataSource、硬體裝置等。如果有多個例項會造成相互衝突、結果不一致的問題,畢竟你有我也有,但是你有的和我有的不一定真的一模一樣,是同一個。使用單例模式可以確保一個類最多隻有一個例項,並提供一個全域性的訪問點。

public class Test {
    public class ABC {
        public ABC() {
        }

//        private ABC() {  //為這個內部類申明私有的構造方法,外部則無法通過new初始化,只能類自己初始化
//        }
//        ABC n1 = new ABC();
    }

    public class CDB {
        public CDB() {
            ABC n1, n2;
            n1 = new ABC();
            n2 = new ABC();
            System.out.println("CBD: " + (n1 == n2));    //false
        }
    }

    public static void main(String[] args) {
        ABC n1, n2;
        n1 = new Test().new ABC();
        n2 = new Test().new ABC();
        System.out.println("main: " + (n1 == n2));   //false
        new Test().new CDB();
    }
}
複製程式碼

那麼有什麼方法可以使得每次new出來的物件都是同一個呢,看看下面單例模式類圖,就可以找到一些思路了!

Singleton(單例)
static uniqueInstance(靜態的唯一物件申明)
private singleton() (私有的例項化方法)
static getInstance() (全域性訪問點)

編碼實戰

瞭解了上面的內容,我們來寫一個簡單的單例模式程式碼,程式碼如下:

public class Singleton {
    private static Singleton uniqeInstance = null;    //靜態變數

    private Singleton() {    // 私有的構造方法,外部無法使用
    }

    public static Singleton getInstance() {
        if (uniqeInstance == null) {
            uniqeInstance = new Singleton();
        }
        return uniqeInstance;
    }
}
複製程式碼

靜態變數由於不屬於任何例項物件,是屬於類的,所以在記憶體中只會有一份,在類的載入過程中,JVM為靜態變數分配一次記憶體空間。
===> Java之static靜態關鍵字詳解

這個場景我們想象一下:一個食品工廠,工廠只有一個,然後工廠裡也只有一個鍋,製作完一批食品才能製作下一批,這個時候我們的食品工廠物件就是單例的了,下面就是模擬實現的程式碼,程式碼的單例實現和上面的簡單實現不同,做了優化處理,稍後會解釋為什麼要優化

public class ChocolateFactory {
    private boolean empty;   // 空鍋
    private boolean boiled;  // 加熱
    public volatile static ChocolateFactory uniqueInstance = null;

    private ChocolateFactory() {
        empty = true;    // 鍋是空的
        boiled = false;  // 還沒加熱
    }

    public static ChocolateFactory getInstance() {
        if (uniqueInstance == null) {
            synchronized (ChocolateFactory.class) {
                if (uniqueInstance == null) {
                    uniqueInstance = new ChocolateFactory();
                }
            }
        }
        return uniqueInstance;
    }
    // 第一步裝填
    public void fill() {
        if (empty) {  // 鍋是空的
            // 新增原料巧克力動作
            empty = false;  // 鍋裝滿了,不是空的
            boiled = false;  // 還沒加熱
        }
    }
    // 第三步倒出
    public void drain() {
        if ((!empty) && boiled) {  // 鍋不是空的,已經加熱
            // 排出巧克力動作
            empty = true;   //出鍋,鍋空了
        }
    }
    // 第二步加熱
    public void boil() {
        if ((!empty) && (!boiled)) {  // 鍋不是空的,沒加熱
            // 煮沸
            boiled = true;  // 已經加熱
        }
    }
}

複製程式碼

單例模式的問題及優化

問題

在多執行緒的情況下,會有時間片的概念,cpu競爭,這剛好就是單例模式可能會發生問題的時候,會發生什麼樣的問題呢?以食品加工廠程式碼為例

public synchronized static ChocolateFactory getInstance() {
       if (uniqueInstance == null) {
           uniqueInstance = new ChocolateFactory();
       }
       return uniqueInstance;
   }
複製程式碼

在多執行緒情況下會例項化出兩個物件

優化解決

同步(synchronized)getInstance方法

執行緒1執行到if (uniqueInstance == null),被執行緒2搶走了執行權,此時執行緒1還沒有new物件;執行緒2同樣來到if (uniqueInstance == null),發現沒有物件例項,也打算例項化物件;最後執行緒1執行緒2都會執行uniqueInstance = new ChocolateFactory();此時可以在getInstance()方法前加上synchronized修飾符同步方法,但是在多執行緒呼叫比較頻繁的時候,這種方式比較耗費效能。

“急切”建立例項

public class ChocolateFactory {
    public static ChocolateFactory uniqueInstance = new ChocolateFactory();  //“急切”建立例項
    public static ChocolateFactory getInstance() {
       if (uniqueInstance == null) {
           uniqueInstance = new ChocolateFactory();
       }
       return uniqueInstance;
   }
}
複製程式碼

public static ChocolateFactory uniqueInstance = new ChocolateFactory();在應用啟動的時候就載入初始化一次例項物件,這個時候多執行緒呼叫永遠也只會有一個例項,因為if (uniqueInstance == null)的結果一直是false;但如果這對單例物件在應用中沒有地方用到,使用這種方式則耗費掉了一些記憶體空間

雙重檢查加鎖(最佳)

public class ChocolateFactory {
    //用volatile修飾的變數,執行緒在每次使用變數的時候,都會讀取變數修改後的最的值。
    public volatile static ChocolateFactory uniqueInstance = null;   
    public static ChocolateFactory getInstance() {
        if (uniqueInstance == null) {
            synchronized (ChocolateFactory.class) {
                if (uniqueInstance == null) {
                    uniqueInstance = new ChocolateFactory();
                }
            }
        }
        return uniqueInstance;
    }
}
複製程式碼

首先public volatile static ChocolateFactory uniqueInstance = null;沒有在應用啟動的時候就初始化物件,節省了記憶體;其次synchronized修飾的程式碼塊是再if (uniqueInstance == null) {}判斷裡面的,只有符合條件才會進入同步方法,減少了效能消耗。

補充

列舉實現單例原理

Java設計模式-單例模式

相關文章