何為單例模式?單例模式即一個類只有一個實體并且該類有提供一個全域訪問點,我們常常希望某個物件實體只有一個,不想要頻繁地創建和銷毀物件,浪費系統資源,這時候我們就要使用單例模式來獲取類的實體,那怎么才能保證一個類是單例的呢?
我們可以先讓一個類的構造方法給私有化,這樣外部就不能通過new來創建類的物件,然后使用靜態變數instance將實體保存起來,當需要使用該類的實體時只要呼叫getInatance()方法就可以得到該類的實體了,代碼如下:
class SingleObject{
private static SingleObject instance;
private SingleObject(){
}
public static SingleObject getInstance(){
if(instance == null){
instance = new SingleObject();
return instance;
}
}
}
但這樣就足夠了嗎?如果只有單個執行緒運行程式的情況下,確實只會回傳一個實體,但如果在多執行緒情況下,還是可能會產生多個實體,為什么?
舉個例子,如果執行緒a在執行完getInstance方法里的if(instance == null)這句代碼后就因為時間片用完變成阻塞狀態,然后執行緒b執行getInstance方法時,此時instance實體還沒有被new出來,所以執行緒b就執行if代碼塊里的代碼創建了一個SingleObject物件,并賦值給instance,然后結束,執行緒a被喚醒后,繼續執行之前的代碼,也就是if代碼塊里的代碼,這時就會產生SingleObject類的多個實體,
為了解決多執行緒情況下可能會產生多個實體的問題,我們可以使用synchronized關鍵字來給產生instance實體的代碼塊“加鎖”解決這個問題:
class SingleObject{
private static SingleObject instance;
public static SingleObject getInstance(){
synchronized (instance){
if(instance == null){
instance = new SingleObject();
return instance;
}
}
return instance;
}
}
通過synchronized關鍵字給創建實體的代碼塊“加鎖”,同一時刻下,只有一個執行緒能夠執行這個代碼塊里的代碼,先執行這個代碼塊的執行緒發現沒有instance實體后會創建instance實體,后面執行的執行緒會因為該實體已經存在而不會再去創建instance實體,
不過這并不是完美的解決方案,只要是鎖,必然有性能損耗問題,而且對于上面的代碼,其實我們只需要在執行緒第一次訪問時加鎖即可,之后并不需要鎖,鎖給我們帶來了系統資源浪費,所以我們可以試著優化一下,如下面代碼:
public class SingleObject {
private static SingleObject instance;
private SingleObject(){
}
public static SingleObject getInstance(){
if (instance == null){
synchronized (instance){
if(instance == null){
instance = new SingleObject();
return instance;
}
}
}
return instance;
}
}
我們可以在同步代碼塊外再加一個判斷物件實體是否存的if陳述句,這樣大部分執行緒執行完第一個if判斷后就不用進入同步代碼塊中,而是直接獲得instance物件,避免了加鎖解鎖的資源消耗,
經過我們使用雙重判斷的優化,看似已經完美解決了多執行緒情況下出現多個實體的問題,其實還留有一個隱患,這個隱患在第二個if判斷的代碼塊里的instance = new SingleObject()這一句代碼上,這句代碼在JVM中不是一個原子操作,而是先有一條位元組碼指令來呼叫SingleObject物件的構造方法,然后下一條位元組碼指令將SingleObject物件的地址賦值給instance參考,但由于JVM的指令重排序優化,這兩條指令的執行順序會發生顛倒,即先將SingleObject物件的地址賦值給instance參考,再呼叫SingleObject物件的構造方法,這樣會出現的情況就是,第一個執行緒在執行將SingleObject物件的地址賦值給instance參考賦值這條指令后,由于cpu時間片用完,而沒有呼叫SingleObject物件的構造方法后就阻塞,接著第二個執行緒在執行第一個if判斷時,此時的instance已經有值,直接return instance,但是此時的instance參考指向的物件并沒有呼叫構造方法,所以是個空物件,這樣就出現了問題,
那該如何解決這個問題呢,我們可以使用volatile關鍵字來修飾instance變數,代碼如下:
public class SingleObject {
private static volatile SingleObject instance;
private SingleObject(){
}
public static SingleObject getInstance(){
if (instance == null){
synchronized (instance){
if(instance == null){
instance = new SingleObject();
return instance;
}
}
}
return instance;
}
}
volatile關鍵字可以禁用被修飾變數的讀寫操作指令的重排序,所以instance = new SingleObject()這一句代碼將會按照先呼叫構造方法、再賦值參考的順序執行,這樣的話,當第一個執行緒進入阻塞狀態時,第二個執行緒在第一個if判斷時會因為instance的值為空而進入第一個if代碼塊中,但if代碼塊中的代碼被synchronized給鎖住,而此時鎖又被第一個執行緒擁有,所以第二個執行緒會進入鎖物件的等待佇列中等待,而當第一個執行緒再次獲得時間片時,它會繼續實體化SingleObject物件并賦值給instance參考,然后結束并釋放鎖,而第二個執行緒被喚醒后拿到鎖執行第二個if判斷,因為instance已經有值,所以直接結束并釋放鎖,這樣就完美地解決了多執行緒模式下可能會產生多個實體的問題,
上面創建單例物件的方式都是在 getInstance() 方法中創建實體,也就是說在要呼叫的時候才創建實體,這種方式被稱為 “ 懶漢式 ” 我們也可以使用“餓漢式”單例模式 ,在類加載時就創建好了實體,代碼如下:
class SingleObject{
private static final SingleObject instance = new SingleObject();
private SingleObject(){
}
public static SingleObject getInstance(){
return instance;
}
}
“餓漢式”的單例模式就是在單例類里面去定義和實體化一個靜態的單例類物件,這樣在單例類被加載時靜態變數的單例物件就會被創建出來,不用去擔心執行緒安全等問題,但是,這樣做的壞處是不管你這個專案用不用到這個單例類物件,該物件都會被創建出來,可能會造成資源浪費,
轉載請註明出處,本文鏈接:https://www.uj5u.com/qianduan/18933.html
標籤:其他
上一篇:學習日志——2019/07/05
