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」第一時間閱讀最新文章。