android懒加载单实例,【 Android 10 设计模式 】系列 -- 单例
前言
由于源碼分析的代碼量比較大,大部分博客網站的內容顯示頁面都比較窄,顯示出來的效果都異常丑陋,所以您也可以直接查看 《 Thinking in Android 》 來閱讀這邊文章,希望這篇文章能幫你梳理清楚 “ 單例模式 ”。
一、概述
1.1 什么是單例?
這種模式涉及到一個單一的類,該類負責創建自己的對象,同時確保只有單個對象被創建。這個類提供了一種訪問其唯一的對象的方式,可以直接訪問,不需要實例化該類的對象。
我們需要注意:
單例類:只能有一個實例(有且唯一)。
單例類:必須自己創建自己的唯一實例(自己創建自己)。
單例類:必須給所有其他對象提供這一實例(提供給別人調用自己的方法)。
1.2 舉個例子
我們看個簡單的 "單例設計" 的 Demo,先創建一個 Singleton(單例) 類:SingleObject.java
public class SingleObject {
//創建 SingleObject 的一個對象
private static SingleObject instance = new SingleObject(); --> 1、有且僅有一個實力;2、自己創建自己的實例
// 讓構造函數為 private,這樣該類就無法被實例化
private SingleObject(){}
// 獲取唯一可用的對象
public static SingleObject getInstance(){ --> 3、為其他對象提供自己的實例
return instance;
}
public void showMessage(){
System.out.println("Hello World!");
}
}
然后我們再創建一個類 SingletonPatternDemo.java 去獲取 SingleObject 的實例:
public class SingletonPatternDemo {
public static void main(String[] args) {
// 不合法的構造函數,編譯時錯誤:構造函數 SingleObject() 是不可見的
//SingleObject object = new SingleObject();
// 獲取唯一可用的對象
SingleObject object = SingleObject.getInstance();
// 顯示消息
object.showMessage();
}
}
我們看下執行結果:
Hello World!
二、實現方式
2.1 基本版:餓漢式
代碼范例
public class Singleton {
private static Singleton instance = new Singleton(); // 類加載時就初始化
private Singleton (){}
public static Singleton getInstance() {
return instance;
}
}
方法說明
這種方式比較常見,典型的 “餓漢式” 寫法。
是否多線程安全
是
實現難度
易
優點
沒有加鎖,執行效率會提高
缺點
類加載時就初始化,浪費內存
2.2 改進版:懶漢式 - 線程不安全
代碼范例
public class Singleton {
private static Singleton instance; // 類加載時不作初始化
private Singleton (){}
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
方法說明
這種方式是大多數面試者的寫法,也是教科書上的標配,但這段代碼卻存在一個致命的問題:當多個線程并行調用 getInstance() 的時候,就會創建多個實例。
是否多線程安全
否
實現難度
易
優點
第一次調用才初始化,避免內存浪費
缺點
當多個線程并行調用 getInstance() 的時候,就會創建多個實例。
2.3 改進版:懶漢式 - 線程安全
代碼范例
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static synchronized Singleton getInstance() { // 加鎖
if (instance == null) {
instance = new Singleton();
}
return instance;
}
}
方法說明
既然要線程安全,那就如上所述進行 “加鎖” 處理!
是否多線程安全
是
實現難度
易
優點
第一次調用才初始化,避免內存浪費
缺點
必須加鎖 synchronized 才能保證單例,但加鎖會影響效率(加鎖操作也是耗時的)
2.4 改進版:雙重校驗鎖
代碼范例
public class Singleton {
private static Singleton instance;
private Singleton (){}
public static Singleton getSingleton() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
方法說明
為什么需要進行 2 次判斷是否為空呢?
第一次判斷是為了避免不必要的同步,第二次判斷是確保在此之前沒有其他進程進入到 synchronized 塊創建了新實例。
這段代碼看起來很完美,很可惜,它還是有隱患!
主要在于 instance = new Singleton() 這句,這并非是一個原子操作,事實上在 JVM 中這句話大概做了下面 3 件事情:
??1、給 instance 分配內存
??2、調用 Singleton 的構造函數來初始化成員變量
??3、將 instance 對象指向分配的內存空間(執行完這步 instance 就為非 null 了)
但是在 JVM 的即時編譯器中存在指令重排序的優化。也就是說上面的第二步和第三步的順序是不能保證的,最終的執行順序可能是 1-2-3 也可能是 1-3-2。如果是后者,則在 3 執行完畢、2 未執行之前,被線程二搶占了,這時 instance 已經是非 null 了(但卻沒有初始化),所以線程二會直接返回 instance,然后使用,最后順理成章地報錯。
2.5 改進版:雙檢鎖(volatile)
代碼范例
public class Singleton {
private volatile static Singleton instance;
private Singleton (){}
public static Singleton getSingleton() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
}
方法說明
有些人認為使用 volatile 的原因是可見性,也就是可以保證線程在本地不會存有 instance 的副本,每次都是去主內存中讀取。但其實是不對的。
使用 volatile 的主要原因是其另一個特性:禁止指令重排序優化。
也就是說,在 volatile 變量的賦值操作后面會有一個內存屏障(生成的匯編代碼上),讀操作不會被重排序到內存屏障之前。比如上面的例子,取操作必須在執行完 1-2-3 之后或者 1-3-2 之后,不存在執行到 1-3 然后取到值的情況。從「先行發生原則」的角度理解的話,就是對于一個 volatile 變量的寫操作都先行發生于后面對這個變量的讀操作(這里的“后面”是時間上的先后順序)。
但是特別注意在 Java 5 以前的版本使用了 volatile 的雙檢鎖還是有問題的。其原因是 Java 5 以前的 JMM (Java 內存模型)是存在缺陷的,即使將變量聲明成 volatile 也不能完全避免重排序,主要是 volatile 變量前后的代碼仍然存在重排序問題。這個 volatile 屏蔽重排序的問題在 Java 5 中才得以修復,所以在這之后才可以放心使用 volatile。
那么,有沒有一種既有懶加載,又保證了線程安全,還簡單的方法呢?
當然有,靜態內部類就是這么一種我們想要的方法。我們完全可以把 Singleton 實例放在一個靜態內部類中,這樣就避免了靜態實例在 Singleton 類加載的時候就創建對象,并且由于靜態內部類只會被加載一次,所以這種寫法也是線程安全的。
2.6 終極版:靜態內部類
代碼范例
public class Singleton {
private static class SingletonHolder {
private static final Singleton INSTANCE = new Singleton();
}
private Singleton (){}
public static final Singleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
方法說明
這是比較推薦的解法,這種寫法用 JVM 本身的機制保證了線程安全的問題,同時讀取實例的時候也不會進行同步,沒什么性能缺陷,還不依賴 JDK 版本。
2.7 傳說版:枚舉
代碼范例
public enum Singleton {
INSTANCE;
}
方法說明
這是從 Java 1.5 發行版本后就可以實用的單例方法,我們可以通過 Singleton.INSTANCE 來訪問實例,這比調用 getInstance() 方法簡單多了。
創建枚舉默認就是線程安全的,所以不需要擔心 double checked locking(雙重校驗鎖),而且還能防止反序列化導致重新創建新的對象。
總結
以上是生活随笔為你收集整理的android懒加载单实例,【 Android 10 设计模式 】系列 -- 单例的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: 虚拟机中ubuntu可以使用显卡吗_在K
- 下一篇: 在微型计算机系统中,打印机一般是通过(