單例模式的含義:
程序運行過程中,希望某個實體(對象)在內存中只有一個實例。
使用條件:
- 該實例的創(chuàng)建比較消耗資源;
- 該實例有單一性,比如某個 Fragment,不希望每次使用時就重新創(chuàng)建。
比如使用 Glide 加載圖片,獲取 Glide 對象的過程就是一個單例模式。因為 Glide 對象的創(chuàng)建過程相當復雜,而且在使用的過程中只需要一個可用 Glide 對象。
public static Glide get(Context context) {
if(glide == null) {
Class var1 = Glide.class;
synchronized(Glide.class) {
if(glide == null) {
Context applicationContext = context.getApplicationContext();
List modules = (new ManifestParser(applicationContext)).parse();
GlideBuilder builder = new GlideBuilder(applicationContext);
...
glide = builder.createGlide();
...
}
}
}
return glide;
}
單例模式的創(chuàng)建:
一、餓漢式
一般來說創(chuàng)建對象首先想到構造函數(shù),那么單例的創(chuàng)建可用先從某對象的構造函數(shù)說起。
- 首先來說因為該類的單例對象只創(chuàng)建一次,可用考慮使用 static 修飾,這樣在 JVM 加載該類的時候就會自動創(chuàng)建對象;
- 接下來我們不希望其他類執(zhí)行該單例類的構造方法再去創(chuàng)建單例對象,所以把構造函數(shù)的屬性設置為 private;(感嘆 Java 的封裝設計)
- 那么存在單例對象就需要把這個對象暴露出去,通過 public 的方法無疑是一種比較好的方式。
這里為什么不直接把單例設置為 public 呢?這里就聯(lián)系到設計原則:迪米特原則了,其他類對該單例類了解的盡量少。其他類獲取該單例類的對象只需要通過其方法暴露出來即可,而不需要了解單例具體是怎么創(chuàng)建的。假如該單例類創(chuàng)建的過程變得更加復雜,其他類的調用還是通過這個簡單的方法獲得對象而不用關心單例類增加了哪些代碼。
public class Singleton {
// 私有單例變量,在 JVM 加載時就會自動創(chuàng)建
private static Singleton INSTANCE = new Singleton();
// 創(chuàng)建私有構造方法,避免其他類再創(chuàng)建對象
private Singleton(){
}
// 向其他類暴露獲取實例的方法
public static Singleton newInstance(){
return INSTANCE;
}
}
那么這種創(chuàng)建方式就是 餓漢式,這種創(chuàng)建方式特點:
- 優(yōu)點:創(chuàng)建簡單、線程安全:因為依賴 JVM 的類加載機制,執(zhí)行類初始化的時候 JVM 會獲取一個鎖,可以同步多個線程對一個類完成初始化
- 缺點:不適合創(chuàng)建復雜,占用內存大的單例對象
- 應用場景:初始化簡單、創(chuàng)建速度快
二、懶漢式
1. 懶漢式(普通使用)
或許我們需要更加靈活地創(chuàng)建和銷毀某個單例對象,又或許需要使用的單例對象比較復雜占用內存比較多,所以需要一種靈活的按需創(chuàng)建的方式:
- 與之前的邏輯類似,依舊不想被其他類通過構造方法或直接獲取單例對象,所以構造函數(shù)和單例對象應該是 private 的;
- 需要一個方法把單例對象暴露出去,這個方法中靈活創(chuàng)建單例對象。
public class Singleton {
// 定義私有變量并設置初始為 null
private static Singleton INSTANCE = null;
// 創(chuàng)建私有構造方法,避免其他類再創(chuàng)建對象
private Singleton(){
}
// 向其他類暴露獲取實例的方法,需要時再創(chuàng)建
public static Singleton newInstance(){
if (INSTANCE == null){
INSTANCE = new Singleton();
}
return INSTANCE;
}
}
這種創(chuàng)建方式就是懶漢式,但是這種方式存在明顯的問題,就是線程安全問題:
-
缺陷:在多線程的情況下調用獲取單例對象 INSTANCE,假設線程 A 調用
newInstance()時單例對象還未創(chuàng)建,執(zhí)行創(chuàng)建過程。線程 B 也來到這個方法來判斷單例對象是否創(chuàng)建,這時由線程 A 申請創(chuàng)建的單例對象還未創(chuàng)建成功,于是又執(zhí)行一次創(chuàng)建過程。這樣在內存中就存在兩個 Singleton 對象的實例,單例模式也就失去了意義。
2. 懶漢式(同步鎖)
既然這種方式有缺陷而且是線程安全問題,那么就加個同步鎖 synchronized 來訪問創(chuàng)建單例對象的部分:
public class Singleton {
// 定義私有變量并設置初始為 null
private static Singleton INSTANCE = null;
// 創(chuàng)建私有構造方法,避免其他類再創(chuàng)建對象
private Singleton(){
}
// 加入同步鎖,確保同一時刻只有一個線程訪問該方法
public static synchronized Singleton newInstance(){
if (INSTANCE == null){
INSTANCE = new Singleton();
}
return INSTANCE;
}
// *第二種寫法
=========================================================
public static Singleton newInstance(){
if (INSTANCE == null){
synchronized (Singleton.class){
INSTANCE = new Singleton();
}
}
return INSTANCE;
}
}
- 優(yōu)點:防止多個線程同步訪問
newInstance()反復創(chuàng)建實例 - 缺點:每次訪問都要進行線程同步,造成過多的同步開銷(加鎖 = 耗時、耗能)
3. 懶漢式(雙重校驗)
public class Singleton {
// 使用 volatile 關鍵字
private static volatile Singleton INSTANCE = null;
// 創(chuàng)建私有構造方法,避免其他類再創(chuàng)建對象
private Singleton(){
}
public static Singleton newInstance(){
// 第一重判斷
if (INSTANCE == null){
synchronized (Singleton.class){
// 第二重判斷
if(INSTANCE == null){
INSTANCE = new Singleton();
}
}
}
return INSTANCE;
}
}
- 優(yōu)點:多加入一層判斷,一定程度上避免多次進入同步鎖方法。
volatile關鍵字保證線程在使用該變量時,都去堆里拿最新的數(shù)據(jù)。 - 缺點:判斷略多,容易混淆。
三、靜態(tài)內部類
利用 Java 靜態(tài)類的特點來創(chuàng)建單例對象。
public class Singleton {
private static class SingletonStatic{
// 在靜態(tài)內部類中加載外部類靜態(tài)對象
private static Singleton INSTANCE = new Singleton();
}
// 私有構造器
private Singleton(){
}
// 獲取靜態(tài)內部類實例化的對象
public static Singleton newInstance(){
return SingletonStatic.INSTANCE;
}
}
上面代碼可以看出:
- 使用靜態(tài)內部類來創(chuàng)建外部類靜態(tài)對;
- 使用
newInstance()時才會加載靜態(tài)內部類,創(chuàng)建單例對象并返回; - 該靜態(tài)內部類只會被 JVM 加載一次,利用這個特性解決線程同步問題。
四、枚舉方式創(chuàng)建單例
public enum Singleton {
INSTANCE;
// 隱藏默認的空的私有構造方法
// private Singleton(){}
}
// 使用
Singleton singleton = Singleton.INSTANCE;
這種創(chuàng)建方式利用枚舉的特性保證了 按需加載、線程同步。
參考資料: