一、問題場景
針對唯一遞增ID号碼生成器,如果需要時就直接建立一個ID對象,當程式中有兩個對象,那就會存在生成重複ID的情況,此外頻繁建立ID對象,也會消耗記憶體空間。
二、問題分析
ID生成器類是全局使用的類,會被頻繁建立和銷毀,為了節省記憶體空間,就要控制建立對象的數量,保證一個類隻有一個對象,并提供一個全局的通路點。這就是建立型設計模式中的單例模式。
三、單例定義
單例設計模式指一個類隻允許建立一個對象,并提供一個全局通路該對象的靜态方法。
四、單例實作方式
為了保證這個對象隻有一個,不能被外部程式建立,這個對象的構造函數必須是私有的,隻能目前類内部建立。
1、餓漢式
public class SingleTon {
/**
* 将構造器私有化,防止直接new
*/
private SingleTon(){
}
/**
* 方式一:靜态常量方式
* SingleTon對象執行個體在定義靜态常量時建立
* 在類裝載時就完成執行個體化
*/
private static SingleTon instance = new SingleTon();
// /**
// * 方式二: 靜态代碼塊方式
// * SingleTon對象執行個體的建立放在靜态代碼塊中
// * 當類裝載的時候,就執行靜态代碼塊中的代碼,初始化SingleTon對象
// */
// private static SingleTon instance;
// static {
// instance = new SingleTon();
// }
/**
* 提供一個public的靜态方法,傳回instance
* @return
*/
public static SingleTon getInstance(){
return instance;
}
}
在類加載時建立并初始化好對象。這種建立是線程安全的,不過不支援延遲加載。
結論:不推薦實際開發中使用
2、懶漢式
public class SingleTonLazyLoading {
/**
* 将構造器私有化,防止直接new
*/
private SingleTonLazyLoading(){
}
private static SingleTon instance;
/**
* 調用擷取單例對象方法時,再建立對象,實作延遲加載
* 加鎖和判空是為了線程安全,防止二次建立對象
* @return
*/
public static Synchronized SingleTonLazyLoading getInstance(){
if(instance == null){
instance = new SingleTon();
}
return instance;
}
}
為了支援懶加載,在擷取單例對象方法時再建立對象;
為了支援線程安全,隻建立一個對象,在擷取單例對象方法上加鎖synchronized,同時方法裡判斷對象是否存在,存在則直接傳回。
懶漢式支援懶加載,但是每次調用該方法,需要頻繁加鎖,釋放鎖,有并發度低的問題。
結論:不推薦實際開發中使用
3、雙重檢測
public class SingleTonDoubleCheck {
/**
* 将構造器私有化,防止直接new
*/
private SingleTonDoubleCheck(){
}
private static SingleTon instance;
public static SingleTonDoubleCheck getInstance(){
if(instance == null){
synchronized (SingleTonDoubleCheck.class){
if(instance == null){
instance = new SingleTon();
}
}
}
return instance;
}
}
懶漢式并發度低,是因為頻繁加鎖和釋放鎖,可以将方法鎖替換成同步代碼塊鎖來縮小加鎖的範圍,進而降低加鎖的頻率。
第一次檢測,是為了判斷對象是否存在,存在則直接傳回對象。
第二次檢測,是為了防止二次建立對象。如果兩個線程都通過了第一次檢測,其中一個線程先擷取鎖,建立好對象之後釋放鎖。然後,第二個線程擷取到鎖,如果不加第二次檢測,就會二次建立對象。
這種實作方式解決了并發度低的問題,既支援延遲加載,也支援高并發。
結論:推薦實際開發中使用
4、靜态内部類
public class SingleTonStaticInnerClass {
/**
* 将構造器私有化,防止直接new
*/
private SingleTonStaticInnerClass(){
}
private static class SingleTonInstance {
private static final SingleTonStaticInnerClass instance = new SingleTonStaticInnerClass();
}
public static SingleTonStaticInnerClass getInstance(){
return SingleTonInstance.instance;
}
}
餓漢式保證了線程安全,但是不支援延遲加載。
要想實作延遲加載,還可以利用靜态内部類的加載特點:在外部類加載的時候,靜态内部類
并不會被加載,隻有在程式中調用靜态内部類的時候才加載。
此外,類裝載的機制又保證初始化執行個體時隻有一個線程
靜态内部類實作方式,既避免了線程不安全,又實作了延遲加載,效率高
結論:推薦實際開發中使用
5、枚舉
public class Singleton {
private Singleton(){
}
public static enum SingletonEnum {
SINGLETON;
private Singleton instance = null;
private SingletonEnum(){
instance = new Singleton();
}
public Singleton getInstance(){
return instance;
}
}
}
因為Java虛拟機會保證枚舉類型不能被反射并且構造函數隻被執行一次。
這種借助JDK1.5中添加的枚舉來實作的單例模式,不僅能避免多線程同步問題,而且還能防
止反序列化重新建立新的對象。
結論:推薦實際開發中使用
五、單例的應用
在JDK中,java.lang.Runtime就是經典的單例模式(餓漢式)
/**
* Every Java application has a single instance of class
* <code>Runtime</code> that allows the application to interface with
* the environment in which the application is running. The current
* runtime can be obtained from the <code>getRuntime</code> method.
* <p>
* An application cannot create its own instance of this class.
*
* @author unascribed
* @see java.lang.Runtime#getRuntime()
* @since JDK1.0
*/
public class Runtime {
private static Runtime currentRuntime = new Runtime();
public static Runtime getRuntime() {
return currentRuntime;
}
/** Don't let anyone else instantiate this class */
private Runtime() {}
//....
public void addShutdownHook(Thread hook) {
SecurityManager sm = System.getSecurityManager();
if (sm != null) {
sm.checkPermission(new RuntimePermission("shutdownHooks"));
}
ApplicationShutdownHooks.add(hook);
}
//...
}
六、單例的使用場景
1、需要頻繁的進行建立和銷毀的對象、建立對象時耗時過多或耗費資源過多(重量級對象),但又經常用到的對象、工具類對象、頻繁通路資料庫或檔案的對象(比如資料源、session工廠等
2、表示全局唯一的類。從業務概念上,如果有些資料在系統中隻應儲存一份,那就比較适合設計為單例類。
比如,配置資訊類。在系統中,我們隻有一個配置檔案,當配置檔案被加載到記憶體之後,以對象的形式存在,也理所應當隻有一份。
七、問題實作
單例模式已經講解清楚,那開篇提到的唯一遞增ID對象的單例模式怎麼設計實作呢?你自己可以先試試實作一下,下期文章揭曉答案。