單例設計模式,意味着整個系統中只能存在一個實例,比方說像日誌對象這種。咱們常說的有餓漢式和懶漢式這兩種模式來建立單例對象,今天就拓展一下思惟,多看幾種。html
首先咱們如果想一個類只有一個對象,那確定先要私有化構造器,斷了在其它的類中使用構造器建立實例的念頭。其它的類中不能建立,咱們就只能在類中本身建立一個私有實例,另外還要提供一個共有的方法使其它對象獲取到實例。因此,初版出現了。設計模式
1 【餓漢式 V1】安全
在類加載的時候就建立實例bash
@ThreadSafe
public class SingletonExample2 {
//
私有化構造器
private SingletonExample2(){}
//
提供一個實例
private static SingletonExample2 instance = new SingletonExample2();
//
提供共有的方法返回實例
public static SingletonExample2 getInstance(){
return
instance;
}
}
|
不要忘了在多線程環境中還有關注線程是否安全,我這裏都會打上註解,@ThreadSafe 表示線程安全,@NotThreadSafe 表示線程不安全。多線程
上面這種方式就是比較簡單的,也是最容易想到的方式,就有一個缺點,如果不使用這個對象,那就有點浪費資源了,這個對象不必定會被使用,可是咱們已經建立好了。性能
2 【餓漢式 V2】優化
這種方式是藉助於 「靜態代碼塊只會被加載一次」 來實現單例的建立,很簡單,也很好理解,問題和餓漢式同樣,不必定就會使用到這個對象,因此可能會出現浪費資源的狀況。spa
@ThreadSafe
public class SingletonExample6 {
//
私有化構造器
private SingletonExample6(){}
private static SingletonExample6 instance = null;
static {
instance = new SingletonExample6();
}
//
提供共有的方法返回實例
public static SingletonExample6 getInstance(){
return
instance;
}
}
|
3 【懶漢式 V1】線程
在對象使用的時候才建立實例設計
@NotThreadSafe
public class SingletonExample1 {
//
私有化構造器
private SingletonExample1(){}
//
提供一個實例
private static SingletonExample1 instance = null;
//
提供共有的方法返回實例
public static SingletonExample1 getInstance(){
if
(instance == null){
return
new SingletonExample1();
}
return
instance;
}
}
|
這種方式在單線程的時候是沒有問題的,可是在多線程時就會出現問題,假如線程 A 進入 if 以後暫停執行,此時又來一個線程 B 仍是能夠進入 if 並返回一個實例,此時 A 再次得到執行時,返回的是另外一個實例了。
4 【懶漢式 V2】
在共有方法上添加 synchronized 關鍵字,同步該方法。可行,可是不推薦使用,由於 synchronized 修飾方法以後,在同一時刻只能有一個線程執行該方法,一旦有線程得到方法,其它線程須要等待,這樣會浪費大量時間,系統運行效率下降。
@ThreadSafe
@NotRecommend
public class SingletonExample3 {
//
私有化構造器
private SingletonExample3(){}
//
提供一個實例
private static SingletonExample3 instance = null;
//
提供共有的方法返回實例
public static synchronized SingletonExample3 getInstance(){
if
(instance == null){
return
new SingletonExample3();
}
return
instance;
}
}
|
5 【懶漢式 V3】
這種方式使用雙重檢測 + 防止指令重排的方式來保證線程安全,首先須要注意的是在 getInstance 方法中,咱們須要雙層檢測並使用同步代碼塊將建立對象的過程同步起來。
@NotThreadSafe
public class SingletonExample4 {
//
私有化構造器
private SingletonExample4(){}
//
提供一個實例
private static SingletonExample4 instance = null;
//
提供共有的方法返回實例
public static SingletonExample4 getInstance(){
//
線程 B 判斷,發現 instance 不爲空,直接返回,而實際上 instance 尚未初始化。
if
(instance == null){
//
雙重檢測機制
synchronized (SingletonExample4.class) {
//
同步鎖
if
(instance == null){
//
線程 A 執行到重排後的指令 3 ,此時 instance 已經有地址值了。可是沒有初始化
return
new SingletonExample4();
//
這裏是重點!!
}
}
}
return
instance;
}
}
|
由於在 new SingletonExample4() 的過程當中,並非一個原子操做,是能夠進一步拆分爲:
一、分配對象內存空間
memory = allocate()
二、初始化對象
initInstance()
三、設置 instance 指向剛分配的內存
instance = memory
在多線程的狀況下,上面 3 個指令會存在指令重排序的狀況。【JVM 和 CPU 指令優化】重排後的結果可能爲:
memory = allocate()
instance = memory
initInstance()
此時可能會存在線程 A 在內層 if 執行到指令重排後的第 3 步,但並未初始化,只是存在了地址值,線程 B 在外層 if 判斷時,會直接 return 實例,而這個實例是一個只有地址值而沒有被初始化的實例。
爲了防止指令重排帶來的問題呢,咱們就可使用 volatile 關鍵字防止指令重排。這樣就是線程安全的了。只需在上一版的基礎上使用 volatile 修飾 instance 實例便可。
volatile 的語義就是添加內存屏障和防止指令重排,這在前面已經分析過了。
|
private static volatile SingletonExample4 instance = null;
6 【使用枚舉類實現單例模式】
這是推薦使用的方法,由於它比懶漢式的線程安全更容易保證,比餓漢式的性能高,它只有在調用的時候才實例對象。
@ThreadSafe
@Recommend
public class SingletonSpecial {
private SingletonSpecial(){}
public static SingletonSpecial getInstance(){
return
Singleton.INSTANCE.getInstance();
}
private enum Singleton{
INSTANCE;
//
public static final Singleton INSTANCE;
private SingletonSpecial singleton;
//
JVM 來保證這個構造方法只會調用一次
Singleton(){
singleton = new SingletonSpecial();
}
public SingletonSpecial getInstance(){
return
singleton;
}
}
}
|
7 【使用靜態內部類】
這種方式在 Singleton 類被裝載時並不會當即實例化,而是在須要實例化時,調用getInstance方法,纔會加載 SingletonInstance 類,從而完成 Singleton 的實例化。
使用 static final 修飾以後 JVM 就會保證 instance 只會初始化一次且不會改變。
@ThreadSafe
@Recommend
public class SingletonExample7 {
private SingletonExample7(){}
private static class SingletonInstance{
private static final SingletonExample7 instance = new SingletonExample7();
}
public static SingletonExample7 getInstance(){
return
SingletonInstance.instance;
}
}
|
總結一下,今天主要說了單例模式的實現,而且在這中間,複習了一下前面說的線程安全的應用。如果對線程安全的原理以及實現有不懂的能夠回頭看看前面幾篇文章。
原文地址:https://www.cnblogs.com/YJK923/p/10516178.html