一、引言
還記得老師當初給我們講單例模式嗎? 小編還清楚記得老師講了一個是餓漢式一個是懶漢式,也講了兩者的實現方式。
那個時候不理解設計模式是做什麼的,就死記硬背記住了,應付一下面試什麼的。
如果你只知道兩種寫法看完文本肯定會有所收穫,如果你是大牛,那就可以點點贊什麼的哈哈哈哈哈
單例模式使用場景:
如果系統中有比較重量級的對象,並且只需要實例化一個的時候,就考慮使用單例模式。舉個實際例子,在實際業務中難免會把數據存儲到Elasticsearch(搜索引擎),那麼對於操作Elasticsearch來說,只需要實例化一個對象即可,這個對象負責與Elasticsearch進行數據交互,看下實際代碼如下:
一個很簡單的餓漢式單例模式,把實例化的過程寫在靜態塊當中,根據不同的啓動環境連接不同的地址的Elasticsearch,最後創建對象賦值給esOperateService。
/**
* @Auther: IT 賤男
* @Date: 2018/11/1 16:15
* @Description: Elasticsearch 代理對象,使用單例模式餓漢式 - 靜態塊實現
*/
public class EsServiceProxy {
private static EsOperateService esOperateService;
// 私有構造,防止外部new
private EsServiceProxy() {
}
// 提供獲取實例的靜態方法
public static EsOperateService getEsOperateService() {
return esOperateService;
}
static {
String env = Foundation.server().getEnvType();
if (StringUtils.isEmpty(env)) {
throw new RuntimeException("環境變量env未配置,請檢查配置!");
}
env = env.toLowerCase();
String baseurl = "http://192.168.188.21:8080/es/esOperateService";
if (!env.equals("dev") && !env.equals("fat")) {
if (env.equals("uat")) {
baseurl = "http://192.168.188.22:8081/es/esOperateService";
} else if (env.equals("pro")) {
baseurl = "http://192.168.13.10:9082/es/esOperateService";
}
}
// 通過代理工廠創建對象並且賦值給常量
HessianProxyFactory hessianProxyFactory = new HessianProxyFactory();
hessianProxyFactory.setOverloadEnabled(true);
try {
esOperateService = (EsOperateService) hessianProxyFactory.create(EsOperateService.class, baseurl);
} catch (MalformedURLException e) {
e.printStackTrace();
}
}
}
二、單例模式餓漢式 - 靜態常量
餓漢式在對象實例化的時候,就會創建好對象,沒有達到懶加載的效果(懶加載:就是說這個對象我不要一開始就創建,等到我需要用的時候在創建),但是這樣就不會因爲多線程的問題導致創建多個實例。
如果保證這個對象,在系統中一定會有用到,那麼這種方式也是推薦使用的。
/**
* @Auther: IT賤男
* @Date: 2019/7/25 15:22
* @Description: 單例模式 餓漢式 - 靜態常量
*
* 優點:寫法比較簡單,在類裝載的時候就完成實例化,避免了多線程的問題
* 缺點:這種方式沒有達到懶加載的效果,可能會造成內存浪費 (如果系統中一定會用到這個對象,則就避免了內存浪費)
*
*/
public class SingletonCase1 {
// 私有構造方法,避免外部new
private SingletonCase1() {
}
// 創建靜態常量
private static final SingletonCase1 singleton = new SingletonCase1();
// 給外部提供實例獲取方法
public static SingletonCase1 getSingleton() {
return singleton;
}
}
三、單例模式餓漢式 - 靜態代碼塊
如果保證這個對象,在系統中一定會有用到,那麼這種方式也是推薦使用的。
/**
* @Auther: IT賤男
* @Date: 2019/7/25 15:22
* @Description: 單例模式 餓漢式 - 靜態代碼
*
* 優缺點和餓漢式靜態常量一致
*
*/
public class SingletonCase2 {
// 私有構造方法,避免外部new
private SingletonCase2() {
}
// 創建靜態常量
private static SingletonCase2 singleton;
static {
// 將對象的實例化放在靜態塊當中,則可以編寫一些邏輯代碼,如文章一開始舉的實戰例子
singleton = new SingletonCase2();
}
// 給外部提供實例獲取方法
public static SingletonCase2 getSingleton() {
return singleton;
}
}
四、單例模式懶漢式 - 線程不安全寫法
懶漢式:可以這樣去記憶理解,既然是懶漢式就是在類加載的時候,懶得去創建對象,等到要用的時候在創建,這樣也就實現了懶加載的效果。 但是這樣實現也會存在一個很嚴重的問題,那就是在多線程的情況下,會存在創建多個實例的現象,具體看如下實現代碼。
這種方式不推薦使用
/**
* @Auther: IT賤男
* @Date: 2019/7/25 15:22
* @Description: 單例模式 懶漢式 - 線程不安全
*
* 優點:可以實現懶加載的效果
* 缺點:在多線程的情況,可能會創建多個實例的情況
*
*/
public class SingletonCase3 {
// 私有構造方法,避免外部new
private SingletonCase3() {
}
// 創建靜態常量
private static SingletonCase3 singleton;
// 給外部提供實例獲取方法
public static SingletonCase3 getSingleton() {
// 這裏會存在多線程的問題,假設線程一正在執行new SingletonCase3()的操作,此時的singleton還是爲null
// 線程二進行 singleton == null 判斷,這個時候等式還是成立的,所以線程二也會執行創建對象的操作。
if (singleton == null) {
singleton = new SingletonCase3();
}
return singleton;
}
}
五、單例模式懶漢式 - 線程安全、同步方法
這個是針對上面的懶漢式進行了改進,給方法加上了synchronized關鍵字,能給有效的解決多線程的問題,但是會影響執行效率,因爲所有的線程都要排隊執行,所以這種方式也不推薦使用。
/**
* @Auther: IT賤男
* @Date: 2019/7/25 15:22
* @Description: 單例模式 懶漢式 - 線程不安全
*
* 優點:可以解決多線程的問題
* 缺點:執行效率太慢,因爲加上synchrionzed所有線程將會排隊等待
*
*/
public class SingletonCase4 {
// 私有構造方法,避免外部new
private SingletonCase4() {
}
// 創建靜態常量
private static SingletonCase4 singleton;
// 給外部提供實例獲取方法
// 這裏給方法上加了synchronized關鍵字,能夠保證只有一個線程執行,其他線程排隊等待
public static synchronized SingletonCase4 getSingleton() {
if (singleton == null) {
singleton = new SingletonCase4();
}
return singleton;
}
}
六、單例模式 - 雙重檢查
經過分析懶漢式,一共存在兩個問題:1 多線程會創建多個、2、執行效率的問題
針對以上兩個問題,所以就有了雙重檢查,這種方式不僅僅避免多線程的問題,還不會影響效率,也實現了懶加載,在公司中也比較常用。
這種方式推薦使用
/**
* @Auther: IT賤男
* @Date: 2019/7/25 15:22
* @Description: 單例模式 雙重檢查
*
* 優點:推薦使用,能夠解決懶加載、多線程的問題
*
* volatile :
* 1、保證此變量對所有線程的可見性,“可見性”指當一條線程修改了這個變量的值,新的值對與其他線程來說是立即得知的。
* 2、禁止指令重排序優化。
*
*/
public class SingletonCase5 {
// 私有構造方法,避免外部new
private SingletonCase5() {
}
/**
*
* 加了volatile關鍵字,能夠解決指令重排的問題,這裏簡單的解釋一下指令重排的問題,需要一點點java內存模型的基礎
*
* 執行這句代碼時 singleton = new SingletonCase5();
*
* 正常情況下是這樣的指令順序
* 1、memory = allocate() 分配對象的內存空間
* 2、ctorInstance() 初始化對象
* 3、instance = memory 設置instance指向剛分配的內存
*
* 在多線程的情況下,JVM 和 CPU優化會發生指令重排,變成這樣了
* 1、memory = allocate() 分配對象的內存空間
* 3、instance = memory 設置instance指向剛分配的內存
* 2、ctorInstance() 初始化對象
*
* A線程在new SingletonCase5()的時候,如果在指令重排以後的情況下,執行到以上的步驟三時,這個時候對象還未初始化
* B線程進執行第一個if判斷的時候,則會直接返回對象,這個時候對象還是未初始化的,如果直接使用則會出現問題
*
*/
// 創建靜態常量
// 這裏給常量加了volatile關鍵字,能夠保證此變量對所有線程對可見性
// 當只要有一個線程修改了這個變量的值,那麼其他線程也就可以立馬獲取到改變之後的值。
private static volatile SingletonCase5 singleton;
// 給外部提供實例獲取方法
public static SingletonCase5 getSingleton() {
// 有些小夥伴在這裏有點疑問,這裏也加了synchronized關鍵字呀,爲什麼不會影響效率呢?
// 小夥伴可以仔細看下代碼,假設線程一進來之後,通過了第一個if判斷,然後進入下面的代碼,並且鎖住了,然後創建了對象
// 然後線程二進來,即使通過了第一個if判斷,等線程一執行完,此時的singleton已不再爲空,所以避免了創建多個對象
// 那之後的線程如果再進來,直接在第一個if判斷就不通過了,不會有線程等待的現象,也不影響效率。
if (singleton == null) {
synchronized (SingletonCase5.class) {
if (singleton == null) {
singleton = new SingletonCase5();
}
}
}
return singleton;
}
}
七、單例模式 - 靜態內部類
這種方式也是可以推薦使用的,也避免了之前懶漢式的問題,編碼也比較簡單。
利用了jvm在裝載類的時候,線程是安全的,也利用了內部類的在加載類時,不會加載內部類,所以這樣寫也是一種方式。
/**
* @Auther: IT賤男
* @Date: 2019/7/25 15:22
* @Description: 單例模式 懶漢式 - 靜態內部類
*
* 優點:
* 1、靜態內部類在類加載的時候,是不會被加載的,實現了懶加載的特性
* 2、在調用獲取實例的方法是,會去裝載內部類,在jvm裝載類的時候線程是安全的,靜態屬性只會在裝載類初始化一次
*
*/
public class SingletonCase6 {
// 私有構造方法,避免外部new
private SingletonCase6() {
}
// 創建靜態內部類,提供常量
private static class SingletonInstance {
private static final SingletonCase6 SINGLETON = new SingletonCase6();
}
// 給外部提供實例獲取方法
public static SingletonCase6 getSingleton() {
return SingletonInstance.SINGLETON;
}
}
八、單例模式 - 枚舉實現
枚舉的實現方式確實能夠達到單例模式所期待的效果,但小編在工作當中也沒有遇到過實際的使用場景。
這種實現方式也不存在有什麼問題,所以也是值得被推薦使用
/**
* @Auther: IT賤男
* @Date: 2019/7/25 15:22
* @Description: 單例模式 懶漢式 - 枚舉實現
* <p>
* 優點:藉助JDK枚舉來實現單例模式,不僅能避免多線程同步的問題,而且還能防止反序列化重新創建新的對象
*/
public class SingletonCase8 {
// 私有構造方法,避免外部new
private SingletonCase8() {
}
// 給外部提供實例獲取方法
public static SingletonCase8 getSingleton() {
return Singleton.INSTANCE.getInstance();
}
private enum Singleton {
INSTANCE;
private SingletonCase8 singletonCase8;
// JVM 保證了這個方法絕對只調用一次
Singleton() {
singletonCase8 = new SingletonCase8();
}
public SingletonCase8 getInstance() {
return singletonCase8;
}
}
}
九、總的來說
值得推薦使用的幾種方式有:
1、如果保證這個對象在項目中一定有使用到,那麼餓漢式是值得推薦使用的,在項目中比較常用。
2、雙重檢查方式推薦使用,在項目中比較常用。
3、靜態內部類、枚舉實現方式推薦使用
那麼這幾種應該是市面上所有的常見的單例模式實現的方式,小編對每一種方式進行分析,也說明了優缺點。小夥伴可以根據不同的業務場景來選擇不同的實現方式。
小編通過簡單文字說明和註釋來講解的單例模式的,如果有小夥伴哪裏有疑惑的可以評論留言,小編看到了會及時回覆的。