1 場景分析
平常開發中,呼叫其他系統的介面是很常見的,呼叫一般需要用到一些配置資訊,而這些配置資訊一般在配置檔案中,程式啟動時讀取到記憶體中使用。
例如有如下配置檔案。
# 檔名 ThirdApp.properties
appId=188210
secret=MIVD587A12FE7E
程式直接讀取配置檔案,解析將配置資訊儲存在一個物件中,呼叫其他系統介面時使用這個物件即可。
package com.chenpi.singleton;
import java.io.IOException;
import java.io.InputStream;
import java.util.Properties;
/**
* @Description 第三方系統相關配置資訊
* @Author 陳皮
* @Date 2021/5/16
* @Version 1.0
*/
public class ThirdConfig {
private String appId;
private String secret;
public ThirdConfig() {
// 初始化
init();
}
/**
* 讀取配置檔案,進行初始化
*/
private void init() {
Properties p = new Properties();
InputStream in = ThirdConfig.class.getResourceAsStream("ThirdApp.properties");
try {
p.load(in);
this.appId = p.getProperty("appId");
this.secret = p.getProperty("secret");
} catch (IOException e) {
e.printStackTrace();
} finally {
try {
if (null != in) {
in.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
}
public String getAppId() {
return appId;
}
public String getSecret() {
return secret;
}
}
使用的時候,直接建立物件,進行使用。
package com.chenpi.singleton;
/**
* @Description
* @Author 陳皮
* @Date 2021/5/16
* @Version 1.0
*/
public class ChenPiMain {
public static void main(String[] args) {
ThirdConfig thirdConfig = new ThirdConfig();
System.out.println("appId=" + thirdConfig.getAppId());
System.out.println("secret=" + thirdConfig.getSecret());
}
}
// 輸出結果如下
appId=1007
secret=陳皮的JavaLib
通過以上分析,需要使用配置資訊時,只要獲取 ThirdConfig 類的例項即可。但是如果專案是多人協作的,其他人使用這個配置資訊的時候選擇每次都去 new 一個新的例項,這樣就會導致每次都會生成新的例項,並且重複讀取配置檔案的資訊,多次 IO 操作,系統中存在多個 AppConfig 例項物件,嚴重浪費記憶體資源,如果配置檔案內容很多的話,浪費系統資源更加嚴重。
針對以上問題,因為配置資訊是不變的,共享的,所以我們只要保證系統執行期間,只有一個類例項存在就可以了。單例模式就用來解決類似這種問題的。
2 單例模式(Singleton)
在系統執行期間,一個類僅有一個例項,並提供一個對外訪問這個例項的全域性訪問點。
將類的構造方法私有化,只能類自身來負責建立例項,並且只能建立一個例項,然後提供一個對外訪問這個例項的靜態方法,這就是單例模式的實現方式。
在 Java 中,單例模式的實現一般分為兩種,懶漢式和餓漢式,它們之間主要的區別是在建立例項的時機上,一種是提前建立例項,一種是使用時才建立例項。
2.1 餓漢式
餓漢式、顧名思義,很飢餓很著急,所以在類載入器裝載類的時候就建立例項,由 JVM 保證執行緒安全,只建立一次,餓漢式實現示例程式碼如下:
package com.chenpi.singleton;
/**
* @Description 餓漢式單例模式
* @Author 陳皮
* @Date 2021/5/16
* @Version 1.0
*/
public class HungerSingleton {
/**
* 提前例項化,由JVM保證例項化一次 使用static關鍵字修飾,使它在類載入器裝載後,初始化此變數,並且能讓靜態方法getInstance使用
*/
private static final HungerSingleton INSTANCE = new HungerSingleton();
/**
* 私有化構造方法,只能內部呼叫,外部呼叫不了則避免了多次例項化的問題
*/
private HungerSingleton() {}
/**
* 外部訪問這個類例項的方法,使用static關鍵字修飾,則外部可以直接通過類來呼叫這個方法
*
* @return HungerSingleton 例項
*/
public static HungerSingleton getInstance() {
return INSTANCE;
}
}
2.2 懶漢式
懶漢式、顧名思義,既然懶就不著急,即等到要使用物件例項的時候才建立例項,更準確的說是延遲載入。懶漢式實現示例程式碼如下:
package com.chenpi.singleton;
/**
* @Description 懶漢式單例模式
* @Author 陳皮
* @Date 2021/5/16
* @Version 1.0
*/
public class LazySingleton {
/**
* 儲存建立好的類例項,賦值null,使用時才建立賦值 因為靜態方法getInstance使用了此變數,所以使用static關鍵字修飾
*/
private static LazySingleton instance = null;
/**
* 私有化構造方法,只能內部呼叫,外部呼叫不了則避免了多次例項化的問題
*/
private LazySingleton() {}
/**
* 外部訪問這個類例項的方法,使用static關鍵字修飾,則外部可以直接通過類來呼叫這個方法
*
* @return LazySingleton 例項
*/
public static LazySingleton getInstance() {
// 判斷例項是否存在
if (instance == null) {
instance = new LazySingleton();
}
// 如果已建立過,則直接使用
return instance;
}
}
3 優缺點比較
餓漢式,當類裝載的時候就建立類例項,不管用不用,後續使用的時候直接獲取即可,不需要判斷是否已經建立,節省時間,典型的空間換時間。
懶漢式,等到使用的時候才建立類例項,每次獲取例項都要進行判斷是否已經建立,浪費時間,但是如果未使用前,則能達到節約記憶體空間的效果,典型的時間換空間。
從執行緒安全性上講, 餓漢式是類載入器載入類到 JVM 記憶體中後,就例項化一個這個類的例項,由 JVM 保證了執行緒安全。而不加同步的懶漢式是執行緒不安全的,在併發的情況下可能會建立多個例項。
如果有兩個執行緒 A 和 B,它們同時呼叫 getInstance 方法時,可能導致併發問題,如下:
public static LazySingleton getInstance() {
// 判斷物件例項是否已被建立
if (instance == null) {
// 執行緒A執行到這裡了,正準備建立例項,或者例項還未建立完,
// 此時執行緒B判斷instance還是為null,則執行緒B也進入此,
// 最終執行緒A和B都會建立自己的例項,從而出現了多例項
instance = new LazySingleton();
}
// 如果已建立過,則直接使用
return instance;
}
所以我們一般推薦餓漢式單例模式,因為由 JVM 例項化,保證了執行緒安全,實現簡單。而且這個例項總會用到的時候,提前例項化準備好也未嘗不可。
4 高階實現
4.1 雙重檢查加鎖
前面說到,懶漢式單例模式在併發情況下可能會出現執行緒安全問題,那我們可以通過加鎖,保證只能一個執行緒去建立例項即可,只要加上 synchronized
即可,如下所示:
public static synchronized LazySingleton getInstance() {
// 判斷物件例項是否已被建立
if (instance == null) {
// 第一次使用,沒用被建立,則先建立物件,並且儲存在類變數中
instance = new LazySingleton();
}
// 如果已建立過,則直接使用
return instance;
}
如果對整個方法加鎖,會降低訪問效能,即每次都要獲取鎖,才能進入執行方法。可以使用雙重檢查加鎖
的方式來實現,就可以既實現執行緒安全,又能夠使效能不受到很大的影響。
雙重檢查加鎖機制:每次進入方法不需要同步,進入方法後,先檢查例項是否存在,如果不存在才進入加鎖的同步塊,這是第一重檢查。進入同步塊後,再次檢查例項是否存在,如果不存在,就在同步的情況下建立一個例項,這是第二重檢查。
使用雙重檢查加鎖機制時,需要藉助 volatile 關鍵字,被它修飾的變數,變數的值就不會被本地執行緒快取,所有對該變數的讀寫都是直接操作共享記憶體,所以能確保多個執行緒能正確的處理該變數。
package com.chenpi.singleton;
/**
* @Description 懶漢式單例模式
* @Author 陳皮
* @Date 2021/5/16
* @Version 1.0
*/
public class LazySingleton {
/**
* 儲存建立好的類例項,賦值null,使用時才建立賦值 因為靜態方法getInstance使用了此變數,所以使用static關鍵字修飾
*/
private volatile static LazySingleton instance = null;
/**
* 私有化構造方法,只能內部呼叫,外部呼叫不了則避免了多次例項化的問題
*/
private LazySingleton() {
}
/**
* 外部訪問這個類例項的方法,使用static關鍵字修飾,則外部可以直接通過類來呼叫這個方法
*
* @return LazySingleton 例項
*/
public static synchronized LazySingleton getInstance() {
// 第一重檢查,判斷例項是否存在,如果不存在則進入同步塊
if (instance == null) {
// 同步塊,能保證同時只能有一個執行緒訪問
synchronized (LazySingleton.class) {
// 第二重檢查,保證只建立一次物件例項
if (instance == null) {
instance = new LazySingleton();
}
}
}
// 如果已建立過,則直接使用
return instance;
}
}
4.2 靜態內部類實現
有一種方式,既有餓漢式的優點(執行緒安全),又有懶漢式的優點(延遲載入),那就是使用靜態內部類。
何為靜態內部類呢?即在類中定義的並且使用 static 修飾的類。可以認為靜態內部類是外部類的靜態成員,靜態內部類物件與外部類物件間不存在依賴關係,因此可直接建立。
靜態內部類中的靜態方法可以使用外部類中的靜態方法和靜態變數;而且靜態內部類只有在第一次被使用的時候才會被裝載,達到了延遲載入的效果。然後我們在靜態內部類中定義一個靜態的外部類的物件,並進行初始化,由 JVM 保證執行緒安全,進行建立。
package com.chenpi.singleton;
/**
* @Description 靜態內部類實現單例模式
* @Author Mr.nobody
* @Date 2021/5/16
* @Version 1.0
*/
public class StaticInnerClassSingleton {
/**
* 靜態內部類,外部內未使用內部類時,類載入器不會載入內部類
*/
private static class SingletonHolder {
/**
* 靜態初始化,由JVM保證執行緒安全
*/
private static final StaticInnerClassSingleton INSTANCE = new StaticInnerClassSingleton();
}
/**
* 私有化構造方法
*/
private StaticInnerClassSingleton() {
}
/**
* 外部訪問這個類例項的方法,使用static關鍵字修飾,則外部可以直接通過類來呼叫這個方法
*
* @return StaticInnerClassSingleton 例項
*/
public static StaticInnerClassSingleton getInstance() {
return SingletonHolder.INSTANCE;
}
}
4.3 列舉實現
還有一種單例模式的最佳實現,就是藉助列舉。實現簡潔,高效安全。沒有構造方法,還能防止反序列化,並由 JVM 保障單例。例項程式碼如下:
package com.chenpi.singleton;
/**
* @Description 列舉實現單例模式,沒有構造方法,能防止反序列化
* @Author 陳皮
* @Date 2021/5/16
* @Version 1.0
*/
public enum EnumSingleton {
/**
* 定義一個列舉的元素,代表要實現類的一個例項
*/
INSTANCE;
// 可以定義方法
public void test() {
System.out.println("Hello ChenPi!");
}
}
如果要使用,直接使用即可,如下:
package com.chenpi.singleton;
/**
* @Description
* @Author 陳皮
* @Date 2021/5/16
* @Version 1.0
*/
public class ChenPiMain {
public static void main(String[] args) {
EnumSingleton instance = EnumSingleton.INSTANCE;
instance.test();
}
}
// 輸出結果
Hello ChenPi!
5 指定數量例項模式
單例模式,只有一個類例項。如果要求指定數量的類例項,例如指定2個或者3個,或者任意多個?
其實萬變不離其宗,單例模式是隻建立一個類例項,並且儲存下來。那指定數量多例模式,就建立指定的數量類例項,也儲存下來,使用時根據策略(例如輪詢)取指定的例項即可。以下演示指定5個例項的情況:
package com.chenpi.singleton;
import java.util.HashMap;
import java.util.Map;
/**
* @Description 指定數量的多例項模式
* @Author 陳皮
* @Date 2021/5/16
* @Version 1.0
*/
public class ExtendSingleton {
/**
* 指定的例項數
*/
private final static int INSTANCE_NUM = 5;
/**
* key字首
*/
private final static String PREFIX_KEY = "instance_";
/**
* 快取例項的容器
*/
private static Map<String, ExtendSingleton> map = new HashMap<>();
/**
* 記錄當前正在使用第幾個例項
*/
private static int num = 1;
/**
* 私有化構造方法,只能內部呼叫,外部呼叫不了則避免了多次例項化的問題
*/
private ExtendSingleton() {
}
/**
* 外部訪問這個類例項的方法,使用static關鍵字修飾,則外部可以直接通過類來呼叫這個方法
*
* @return ExtendSingleton 例項
*/
public static ExtendSingleton getInstance() {
// 快取key
String key = PREFIX_KEY + num;
// 優先從快取中取
ExtendSingleton extendSingleton = map.get(key);
// 如果指定key的例項不存在,則建立,並放入快取容器中
if (extendSingleton == null) {
extendSingleton = new ExtendSingleton();
map.put(key, extendSingleton);
}
// 當前例項的序號加1
num++;
if (num > INSTANCE_NUM) {
// 例項的序號達到最大值重新從1開始
num = 1;
}
return extendSingleton;
}
}
本次分享到此結束啦~~
如果覺得文章對你有幫助,點贊、收藏、關注、評論,您的支援就是我創作最大的動力!
我是陳皮,一個在網際網路 Coding 的 ITer,微信搜尋「陳皮的JavaLib」第一時間閱讀最新文章。