單例模式原理
什麼是單例物件?
有些物件我們只需要一個如執行緒池、快取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) {}
判斷裡面的,只有符合條件才會進入同步方法,減少了效能消耗。