設計模式:這是最全面 & 詳細的 單例模式(Singleton)分析指南
阿新 • • 發佈:2019-01-09
前言
今天我來全面總結一下Android開發中最常用的設計模式 - 單例模式。
關於設計模式的介紹,可以看下我之前寫的:1分鐘全面瞭解“設計模式”
目錄
1. 例項引入
- 背景:小成有一個塑料生產廠,但裡面只有一個倉庫。
- 目的:想用程式碼來實現倉庫的管理
- 現有做法: 建立倉庫類和工人類
其中,倉庫類裡的quantity=商品數量;工人類裡有搬運方法MoveIn(int i)和MoveOut(int i)。
- 出現的問題:通過測試發現,每次工人搬運操作都會新建一個倉庫,就是貨物都不是放在同一倉庫,這是怎麼回事呢?(看下面程式碼)
package scut.designmodel.SingletonPattern;
//倉庫類
class StoreHouse {
private int quantity = 100;
public void setQuantity(int quantity) {
this.quantity = quantity;
}
public int getQuantity() {
return quantity;
}
}
//搬貨工人類
class Carrier{
public StoreHouse mStoreHouse;
public Carrier(StoreHouse storeHouse){
mStoreHouse = storeHouse;
}
//搬貨進倉庫
public void MoveIn(int i){
mStoreHouse.setQuantity(mStoreHouse.getQuantity()+i);
}
//搬貨出倉庫
public void MoveOut(int i){
mStoreHouse.setQuantity(mStoreHouse.getQuantity()-i);
}
}
//工人搬運測試
public class SinglePattern {
public static void main(String[] args){
StoreHouse mStoreHouse1 = new StoreHouse();
StoreHouse mStoreHouse2 = new StoreHouse();
Carrier Carrier1 = new Carrier(mStoreHouse1);
Carrier Carrier2 = new Carrier(mStoreHouse2);
System.out.println("兩個是不是同一個?");
if(mStoreHouse1.equals(mStoreHouse2)){//這裡用equals而不是用 == 符號,因為 == 符號只是比較兩個物件的地址
System.out.println("是同一個");
}else {
System.out.println("不是同一個");
}
//搬運工搬完貨物之後出來彙報倉庫商品數量
Carrier1.MoveIn(30);
System.out.println("倉庫商品餘量:"+Carrier1.mStoreHouse.getQuantity());
Carrier2.MoveOut(50);
System.out.println("倉庫商品餘量:"+Carrier2.mStoreHouse.getQuantity());
}
}
結果:
兩個是不是同一個?
不是同一個
倉庫商品餘量:130
倉庫商品餘量:50
2. 單例模式介紹
2.1 模式說明
實現1個類只有1個例項化物件 & 提供一個全域性訪問點2.2 作用(解決的問題)
保證1個類只有1個物件,降低物件之間的耦合度從上面可看出:工人類操作的明顯不是同一個倉庫例項,而全部工人希望操作的是同一個倉庫例項,即只有1個例項
2.3 工作原理
在Java中,我們通過使用物件(類例項化後)來操作這些類,類例項化是通過它的構造方法進行的,要是想實現一個類只有一個例項化物件,就要對類的構造方法下功夫:
單例模式的一般實現:(含使用步驟)
public class Singleton {
//1. 建立私有變數 ourInstance(用以記錄 Singleton 的唯一例項)
//2. 內部進行例項化
private static Singleton ourInstance = new Singleton();
//3. 把類的構造方法私有化,不讓外部呼叫構造方法例項化
private Singleton() {
}
//4. 定義公有方法提供該類的全域性唯一訪問點
//5. 外部通過呼叫getInstance()方法來返回唯一的例項
public static Singleton newInstance() {
return ourInstance;
}
}
好了,單例模式的介紹和原理應該瞭解了吧?那麼我們現在來解決上面小成出現的“倉庫不是一個”的問題吧!
3. 例項講解
小成使用單例模式改善上面例子的程式碼:package scut.designmodel.SingletonPattern;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
//單例倉庫類
class StoreHouse {
//倉庫商品數量
private int quantity = 100;
//自己在內部例項化
private static StoreHouse ourInstance = new StoreHouse();;
//讓外部通過呼叫getInstance()方法來返回唯一的例項。
public static StoreHouse getInstance() {
return ourInstance;
}
//封閉建構函式
private StoreHouse() {
}
public void setQuantity(int quantity) {
this.quantity = quantity;
}
public int getQuantity() {
return quantity;
}
}
//搬貨工人類
class Carrier{
public StoreHouse mStoreHouse;
public Carrier(StoreHouse storeHouse){
mStoreHouse = storeHouse;
}
//搬貨進倉庫
public void MoveIn(int i){
mStoreHouse.setQuantity(mStoreHouse.getQuantity()+i);
}
//搬貨出倉庫
public void MoveOut(int i){
mStoreHouse.setQuantity(mStoreHouse.getQuantity()-i);
}
}
//工人搬運測試
public class SinglePattern {
public static void main(String[] args){
StoreHouse mStoreHouse1 = StoreHouse.getInstance();
StoreHouse mStoreHouse2 = StoreHouse.getInstance();
Carrier Carrier1 = new Carrier(mStoreHouse1);
Carrier Carrier2 = new Carrier(mStoreHouse2);
System.out.println("兩個是不是同一個?");
if(mStoreHouse1.equals(mStoreHouse2)){
System.out.println("是同一個");
}else {
System.out.println("不是同一個");
}
//搬運工搬完貨物之後出來彙報倉庫商品數量
Carrier1.MoveIn(30);
System.out.println("倉庫商品餘量:"+Carrier1.mStoreHouse.getQuantity());
Carrier2.MoveOut(50);
System.out.println("倉庫商品餘量:"+Carrier2.mStoreHouse.getQuantity());
}
}
結果:
兩個是不是同一個?
是同一個
倉庫商品餘量:130
倉庫商品餘量:80
從結果分析,使用了單例模式後,倉庫類就只有一個倉庫例項了,再也不用擔心搬運工人進錯倉庫了!!!
4. 特點
4.1 優點
- 提供了對唯一例項的受控訪問;
- 由於在系統記憶體中只存在一個物件,因此可以節約系統資源,對於一些需要頻繁建立和銷燬的物件單例模式無疑可以提高系統的效能;
- 可以根據實際情況需要,在單例模式的基礎上擴充套件做出雙例模式,多例模式;
4.2 缺點
- 單例類的職責過重,裡面的程式碼可能會過於複雜,在一定程度上違背了“單一職責原則”。
- 如果例項化的物件長時間不被利用,會被系統認為是垃圾而被回收,這將導致物件狀態的丟失。
5. 單例模式的實現方式
- 單例模式的實現方式有多種,根據需求場景,可分為2大類、6種實現方式。具體如下:
- 下面,我將詳細介紹每種單例模式的實現方式
a. 初始化單例類時 即 建立單例
1. 餓漢式
這是 最簡單的單例實現方式
原理
依賴JVM
類載入機制,保證單例只會被建立1次,即 執行緒安全JVM
在類的初始化階段(即 在Class
被載入後、被執行緒使用前),會執行類的初始化- 在執行類的初始化期間,JVM會去獲取一個鎖。這個鎖可以同步多個執行緒對同一個類的初始化
具體實現
class Singleton {
// 1. 載入該類時,單例就會自動被建立
private static Singleton ourInstance = new Singleton();
// 2. 建構函式 設定為 私有許可權
// 原因:禁止他人建立例項
private Singleton() {
}
// 3. 通過呼叫靜態方法獲得建立的單例
public static Singleton newInstance() {
return ourInstance;
}
}
- 應用場景
除了初始化單例類時 即 建立單例外,繼續延伸出來的是:單例物件 要求初始化速度快 & 佔用記憶體小
2. 列舉型別
- 原理
根據列舉型別的下述特點,滿足單例模式所需的 建立單例、執行緒安全、實現簡潔的需求
- 實現方式
public enum Singleton{
//定義1個列舉的元素,即為單例類的1個例項
INSTANCE;
// 隱藏了1個空的、私有的 構造方法
// private Singleton () {}
}
// 獲取單例的方式:
Singleton singleton = Singleton.INSTANCE;
- 注:這是 最簡潔、易用 的單例實現方式,借用
《Effective Java》
的話:
單元素的列舉型別已經成為實現
Singleton
的最佳方法
b. 按需、延遲建立單例
1. 懶漢式(基礎實現)
- 原理
與 餓漢式 最大的區別是:單例建立的時機
- 餓漢式:單例建立時機不可控,即類載入時 自動建立 單例
- 懶漢式:單例建立時機可控,即有需要時,才 手動建立 單例
- 具體實現
class Singleton {
// 1. 類載入時,先不自動建立單例
// 即,將單例的引用先賦值為 Null
private static Singleton ourInstance = null;
// 2. 建構函式 設定為 私有許可權
// 原因:禁止他人建立例項
private Singleton() {
}
// 3. 需要時才手動呼叫 newInstance() 建立 單例
public static Singleton newInstance() {
// 先判斷單例是否為空,以避免重複建立
if( ourInstance == null){
ourInstance = new Singleton();
}
return ourInstance;
}
}
- 缺點
基礎實現的懶漢式是執行緒不安全的,具體原因如下
- 下面,將對懶漢式 進行優化,使得適合在多執行緒環境下執行
2. 同步鎖(懶漢式的改進)
原理
使用同步鎖synchronized
鎖住 建立單例的方法 ,防止多個執行緒同時呼叫,從而避免造成單例被多次建立- 即,
getInstance()
方法塊只能執行在1個執行緒中 - 若該段程式碼已在1個執行緒中執行,另外1個執行緒試圖執行該塊程式碼,則 會被阻塞而一直等待
- 而在這個執行緒安全的方法裡我們實現了單例的建立,保證了多執行緒模式下 單例物件的唯一性
- 即,
具體實現
// 寫法1
class Singleton {
// 1. 類載入時,先不自動建立單例
// 即,將單例的引用先賦值為 Null
private static Singleton ourInstance = null;
// 2. 建構函式 設定為 私有許可權
// 原因:禁止他人建立例項
private Singleton() {
}
// 3. 加入同步鎖
public static synchronized Singleton getInstance(){
// 先判斷單例是否為空,以避免重複建立
if ( ourInstance == null )
ourInstance = new Singleton();
return ourInstance;
}
}
// 寫法2
// 該寫法的作用與上述寫法作用相同,只是寫法有所區別
class Singleton{
private static Singleton instance = null;
private Singleton(){
}
public static Singleton getInstance(){
// 加入同步鎖
synchronized(Singleton.class) {
if (instance == null)
instance = new Singleton();
}
return instance;
}
}
- 缺點
每次訪問都要進行執行緒同步(即 呼叫synchronized
鎖),造成過多的同步開銷(加鎖 = 耗時、耗能)
實際上只需在第1次呼叫該方法時才需要同步,一旦單例建立成功後,就沒必要進行同步
3. 雙重校驗鎖(懶漢式的改進)
原理
在同步鎖的基礎上,新增1層if
判斷:若單例已建立,則不需再執行加鎖操作就可獲取例項,從而提高效能具體實現
class Singleton {
private static Singleton ourInstance = null;
private Singleton() {
}
public static Singleton newInstance() {
// 加入雙重校驗鎖
// 校驗鎖1:第1個if
if( ourInstance == null){ // ①
synchronized (Singleton.class){ // ②
// 校驗鎖2:第2個 if
if( ourInstance == null){
ourInstance = new Singleton();
}
}
}
return ourInstance;
}
}
// 說明
// 校驗鎖1:第1個if
// 作用:若單例已建立,則直接返回已建立的單例,無需再執行加鎖操作
// 即直接跳到執行 return ourInstance
// 校驗鎖2:第2個 if
// 作用:防止多次建立單例問題
// 原理
// 1. 執行緒A呼叫newInstance(),當執行到②位置時,此時執行緒B也呼叫了newInstance()
// 2. 因執行緒A並沒有執行instance = new Singleton();,此時instance仍為空,因此執行緒B能突破第1層 if 判斷,執行到①位置等待synchronized中的A執行緒執行完畢
// 3. 當執行緒A釋放同步鎖時,單例已建立,即instance已非空
// 4. 此時執行緒B 從①開始執行到位置②。此時第2層 if 判斷 = 為空(單例已建立),因此也不會建立多餘的例項
- 缺點
實現複雜 = 多種判斷,易出錯
4. 靜態內部類
原理
根據 靜態內部類 的特性,同時解決了按需載入、執行緒安全的問題,同時實現簡潔- 在靜態內部類裡建立單例,在裝載該內部類時才會去建立單例
- 執行緒安全:類是由
JVM
載入,而JVM
只會載入1遍,保證只有1個單例
具體實現
class Singleton {
// 1. 建立靜態內部類
private static class Singleton2 {
// 在靜態內部類裡建立單例
private static Singleton ourInstance = new Singleton();
}
// 私有建構函式
private Singleton() {
}
// 延遲載入、按需建立
public static Singleton newInstance() {
return Singleton2.ourInstance;
}
}
// 呼叫過程說明:
// 1. 外部呼叫類的newInstance()
// 2. 自動呼叫Singleton2.ourInstance
// 2.1 此時單例類Singleton2得到初始化
// 2.2 而該類在裝載 & 被初始化時,會初始化它的靜態域,從而建立單例;
// 2.3 由於是靜態域,因此只會JVM只會載入1遍,Java虛擬機器保證了執行緒安全性
// 3. 最終只建立1個單例
6. 總結
- 本文主要對 單例模式 進行了全面介紹,包括原理 & 實現方式
- 對於實現方式,此處作出總結
- 接下來我會繼續講解其他設計模式,有興趣可以繼續關注Carson_Ho的安卓部落格