標籤:java 單例模式 設計模式
單例模式
單例模式在程式設計中使用的頻率非常之高,其設計的目的是為了在程式中提供唯一一個對象(保證只被構造一次),例如寫入日誌的log對象,windows的工作管理員實現(只能開啟一個)。這裡主要介紹單例模式使用Java的實現(包括餓漢式及懶漢式)。
實現
這裡使用Log類作為例子,Log對象需要在程式中只有一個對象且只初始化一次。
餓漢式
餓漢式的單例模式理解起來是比較容易的,就是在單例類載入的時候就初始化需要單例的對象。實現也比較容易。
public class Singleton{ private static Log logObj = new Log(); public static Log getInstance(){ return logObj; }}
懶漢式
如果logObj需要佔用很大的記憶體,如果一開始就初始化logObj,那麼會佔用大量的記憶體。此時,有人就想,如果我在想用的時候再初始化Log類的對象,像懶漢一樣,只有用到的時候再初始化,需要怎麼設計呢?
實現一(非安全執行緒版本)
public class Singleton{ private static Log logObj = null; public static Log getInstance(){ if(logObj == null){ logObj = new Log(); } return logObj; }}
實現二(安全執行緒版本)
public class Singleton{ private static Log logObj = null; public static synchronized Log getInstance(){ if(logObj == null){ logObj = new Log(); } return logObj; }}
為了實現安全執行緒,這個版本的實現犧牲了一定的效率,如果logObj已經初始化,那麼其他線程還需要同步的進入getInstance方法,會造成效率的損失。於是,有些人實現了下面的版本。
實現三(錯誤版本)
public class Singleton{ private static Log logObj = null; public static Log getInstance(){ if(logObj == null){ synchronized(Singleton.class){ if(logObj == null){ logObj = new Log(); } } } return logObj; }}
乍看起來上面的版本是沒問題的,如果某個線程A發現logObj 還沒初始化,那麼就進入同步塊初始化logObj,如果在這期間有其他線程B進入,那麼線程B就會等待進入同步塊,等待A 線程退出同步塊,logObj 已經初始化了,B 線程進入同步塊後發現logObj 不為null,退出同步塊,不再初始化logObj 。 這樣既實現了安全執行緒,又兼顧了效率,確實是很聰明的編碼方式。但是問題來了,由於指令重排序的存在,會導致Log在完全初始化之前logObj就已經不為null。這樣其他線程可能會得到未完全初始化的對象。
解決方案
JDK1.5版本後擴充了volitile語義,可以保證上述代碼的正確性,為此只要將logObj 聲明為volitile即可(volitile之前只是保證記憶體的可見度而已)。
使用靜態內部類。
載入一個類時,其內部類不會同時被載入。一個類被載入,若且唯若其某個靜態成員(靜態域、構造器、靜態方法等)被調用時發生。
並且jvm會保證類載入的安全執行緒問題,所以利用這個特性可以寫出兼顧效率與保證安全執行緒的版本。
實現四(兼顧效率與安全執行緒的版本)
public class Singleton{ static class LogHolder{ static Log logObj = new Log(); } public static Log getInstance(){ return LogHolder.logObj; }}
這樣在Singleton類載入時,並不會載入LogHolder,也就不會初始化Log,如果有線程訪問getInstance方法,那麼jvm會首先載入LogHolder類,並保證初始化logObj,最後返回logObj。
著作權聲明:本文為博主原創文章,未經博主允許不得轉載。
設計模式-單例模式(餓漢式及懶漢式的Java實現)