概述
單例模式,它能保證我們始終如一的使用同一個對象,我們平時經常會去用它,因爲可以避免重複製造對象,減少內存隱患,我們也都可以寫個常見的單例出來。
這裏要講下單例到底應該怎麼寫,既能避免線程不安全,也能保證性能。
內容
1、一個最簡單的單例模式
public class GirlFriend {
//靜態變量記錄唯一實例
private static GirlFriend instance;
//構造器私有化
private GirlFriend() {
}
//靜態實例化方法公有,保證不重複創建對象
public static GirlFriend getInstance() {
if (instance == null) {
instance = new GirlFriend();
}
return instance;
}
}
以上代碼片,給出了單例模式的最基本的三要素:
- 靜態變量記錄唯一實例;
- 構造器私有化;
這樣能保證外界不會通過構造器構造對象。 - 靜態實例化方法公有化,保證不重複創建對象
void test() {
GirlFriend girlFriend = GirlFriend.getInstance();
}
當你在第一次調用getInstance()方法時,會對instance進行初始化,以後再次調用該方法,則不會重複創建對象,而是直接使用已經被實例化的instance對象。
但是呢,這個單例不是線程安全的,假設有兩個線程,都在調用getInstance()方法,其中線程A執行到了“if (instance == null) ”,而另一個線程B執行到“instance = new GirlFriend();”,那麼當B中的instance實例初始化完成前,線程A就已經滿足條件即將進入再次初始化instance,這樣一來,instance實例被初始化了兩次,且A、B兩個線程得到的並不是同一個實例對象,如果同時一百個線程這麼執行,將構造一百次GirlFriend類型變量,所以多線程使用以上單例就需要謹慎了。
2、同步機制
如果想要線程安全,一種最便捷的方式是使用同步“synchronized”,就像這樣:
public class GirlFriend {
private static GirlFriend instance;
private GirlFriend() {
}
public static synchronized GirlFriend getInstance() {
if (instance == null) {
instance = new GirlFriend();
}
return instance;
}
}
使用synchronized關鍵字修飾getInstance()方法,會使得,在多個線程執行此方法時,必須等待其他線程執行完畢後,才能進入該方法,且每次只能有一個線程進入getInstance()方法。
但是,頻繁併發同步時,會意味着,你多個線程總是要等來等去,一個接一個執行被synchronized關鍵字修飾的代碼,這樣會影響程序效率、性能。我們只是需要在第一次進入getInstance()方法時,同步線程避免多次實例化對象。
雙重檢查加鎖
爲了避免多次同步,使用雙重檢查加鎖,如下:
public class GirlFriend {
private volatile static GirlFriend instance;
private GirlFriend() {
}
public static GirlFriend getInstance() {
if (instance == null) {
synchronized (GirlFriend.class) {
if (instance == null) {
instance = new GirlFriend();
}
}
}
return instance;
}
}
分析一下,如果同時線程A和線程B幾乎同時執行getInstance()方法,它們都經歷第一次條件判斷,假設A拿到同步鎖,執行初始化instance的代碼,線程B等候A釋放了同步鎖後,經歷第二次判斷,顯然instance已經不爲空了,所以會釋放鎖,執行“return instance”,避免了重複初始化instance。這樣以後再調用getInstance()方法,第一次條件判斷爲“假”,則避免了synchronized語句,極大的優化了性能。
上面那段話橫豎沒有提到“volatile”關鍵字,但是爲神馬要用它呢?
計算機在執行程序時,每條指令都是在CPU中執行的,而執行指令過程中,勢必涉及到數據的讀取和寫入。由於程序運行過程中的臨時數據是存放在主存(物理內存)當中的,這時就存在一個問題,由於CPU執行速度很快,而從內存讀取數據和向內存寫入數據的過程跟CPU執行指令的速度比起來要慢的多,因此如果任何時候對數據的操作都要通過和內存的交互來進行,會大大降低指令執行的速度。因此在CPU裏面就有了高速緩存。也就是,當程序在運行過程中,會將運算需要的數據從主存複製一份到CPU的高速緩存當中,那麼CPU進行計算時就可以直接從它的高速緩存讀取數據和向其中寫入數據,當運算結束之後,再將高速緩存中的數據刷新到主存當中。舉個簡單的例子,比如下面的這段代碼:
i = i + 1;
當線程執行這個語句時,會先從主存當中讀取i的值,然後複製一份到高速緩存當中,然後CPU執行指令對i進行加1操作,然後將數據寫入高速緩存,最後將高速緩存中i最新的值刷新到主存當中。
在多核CPU中,每個線程都有自己的高速緩存,這就意味着,在多個線程併發讀寫同一個變量時,這種讀寫只是作用於高速緩存中內容。比如以上那句代碼,當線程A和線程B分別從主內存中讀取i的值到自己的CPU高速緩存中,A中的“i=i+1”,則“i”值爲“1”,這個值還在A線程CPU高速緩存中,沒有同步到主內存,而此時線程B正好執行了“i=i+1”,但因爲“i”存在於B線程CPU高速緩存中的值仍然是0,執行“i=i+1”的結果仍然是“i”值爲“1”,最後兩線程同步到主內存中的i的值是“1”。但實際上我們預測的是“i加1然後再加1“,結果是“2”。
volatile:它是一個類型修飾符(type specifier),就像大家更熟悉的const一樣,它是被設計用來修飾被不同線程訪問和修改的變量。volatile的作用是作爲指令關鍵字,確保本條指令不會因編譯器的優化而省略,且要求每次直接讀值。
這就意味着,被volatile修飾的變量,值已經修改,會馬上同步到主內存。所以多個線程每次得到的被volatile修飾的變量,它的值都是經過最近更新的。
所以這裏我們用volatile修飾instance變量,就是爲了讓instance的值和主內存同步。
總結
如果這篇博客幫助到您,您可以完成以下操作:
在網易雲搜索“星河河”->歌手->點擊進入(您將進入我的網易雲音樂人賬號星河河)->關注我->多聽聽我的歌。