驀然回頭-單例模式篇章二
單例模式引發相關整理
關聯執行緒安全
在多執行緒下,懶漢式會有一定修改。當兩個執行緒在if(null == instance)語句阻塞的時候,可能由兩個執行緒進入建立例項,從而返回了兩個物件。對此,我們可以加鎖,保證僅有一個執行緒處於getInstance()方法中,從而保證了執行緒一致性。多執行緒下的單例
/**
* @author sunyang
* @date 2018/11/8 12:18
*/
public class Singleton4 {
private static Singleton4 instance;
private Singleton4(){
}
//需要加上synchronized 同步
public static synchronized Singleton4 getInstance(){
if (instance == null){
instance = new Singleton4();
}
return instance;
}
}
如果一個專案中有100次獲取例項,那麼jvm就會有100次進行加鎖,釋放鎖的操作,每次操作都浪費資源。
可以在最外層再加一層判斷,如下
/**
* @author sunyang
* @date 2018/11/12 19:07
*/
public class Singleton41 {
private static Singleton41 instance;
private Singleton41(){}
private static synchronized void doGetInstance(){
if (null == instance){
instance = new Singleton41();
}
}
public static synchronized Singleton41 getInstance(){
if (null == instance){
doGetInstance();
}
return instance;
}
}
簡化形式後:
/**
* @author sunyang
* @date 2018/11/12 19:13
*/
public class Singleton5 {
private static Singleton5 instance;
private Singleton5(){}
//如果多個執行緒同時通過第一次檢查,並且其中一個執行緒
// 首先通過了第二次檢查並例項化了物件,那麼剩餘通過了
//第一次檢查的執行緒就不會再去例項化物件。提升了效率
public static Singleton5 getInstance(){
if (null == instance){
synchronized (Singleton5.class){
if (null == instance){
instance = new Singleton5();
}
}
}
return instance;
}
}
題外話:一個類在new的時候,一般經歷以下三個順序:
1.開闢空間
2.符號引用改空間,並在空間內對類進行初始化操作
3.將符合引用轉為直接引用這個時候if(null==instance) return false;
在實際的情況中,為了降低CPU的閒置時間,jvm會對指令進行重排序以形成指令流水線。順序可能亂序:
1.開闢空間
2.轉為直接引用
3.初始化類
結論:雙重檢查機制就會出現問題:可能返回一個未被完全初始化的類;
程式碼不安全截圖
1542025736992
volatile單例中的作用
- 可見性:jvm中每一個執行緒都有自己的記憶體區域。對變數使用volatile修飾,可以強制將每一次的讀寫都寫入堆記憶體中,實現了各個執行緒都能共享的最新資料。
- 禁止指令重排序優化:被volatile修飾的變數,在賦值的結尾會插入一個記憶體屏障,從而防止指令重排序。volatile增強了資料的一致性。
/**
* 解決上圖雙重檢查機制出現的問題,可能返回一個未被完全初始化的類
*
* @author sunyang
* @date 2018/11/12 20:24
*/
public class Singleton51 {
private static volatile Singleton51 instance;
private Singleton51(){}
public static Singleton51 getInstance(){
if (null==instance){
synchronized (Singleton51.class){
if (null == instance){
instance = new Singleton51();
}
}
}
return instance;
}
}
如果實現懶載入
先了解下靜態巢狀類的使用
靜態巢狀類:是一種在類之外宣告的巢狀類,由於是靜態的,所以不經過初始化,就可以通過類名直接呼叫。
內部類:該類作為另一個類的成員,因此只有引用另一個類,才能建立這個類。通過靜態巢狀類,便可以實現
對餓漢式進行懶化的效果。
/**
* @author sunyang
* @date 2018/11/12 20:42
*/
public class Singleton6 {
private Singleton6(){}
//靜態內部類
private static class SingletonHolder{
private static Singleton6 INSTANCE = new Singleton6();
}
//通過靜態巢狀類,便可以實現對餓漢式進行懶化的效果
public static final Singleton6 getInstance(){
return SingletonHolder.INSTANCE;
}
}
分析
要分析這種方式有沒有實現懶載入,就要分析一下語句new Singleton6()是什麼時候被呼叫的.
使用javac進行編譯,會得到如下圖的三個class檔案:
1542027696077
從圖上可以看到,靜態巢狀類是單獨作為一個class存在,而其中建立物件的邏輯位於巢狀類中,jvm讀取巢狀類的位元組碼以後才能建立物件,從硬碟中讀取class檔案,在記憶體中分配空間,是一件費事費力的工作,所以jvm選擇按需載入,沒有必要載入的就不載入,沒必要分配就不分配。