在Java開發過程中,很多場景下都會碰到或要用到單例模式,在設計模式里也是經常作為指導學習的熱門模式之一,相信每位開發同事都用到過。我們總是沿着前輩的足跡去做設定好的思路,往往沒去探究為何這么做,所以這篇文章對單例模式做了詳解。
一、單例模式定義:
單例模式確保某個類只有一個實例,而且自行實例化並向整個系統提供這個實例。在計算機系統中,線程池、緩存、日志對象、對話框、打印機、顯卡的驅動程序對象常被設計成單例。這些應用都或多或少具有資源管理器的功能。每台計算機可以有若干個打印機,但只能有一個Printer Spooler,以避免兩個打印作業同時輸出到打印機中。每台計算機可以有若干通信端口,系統應當集中管理這些通信端口,以避免一個通信端口同時被兩個請求同時調用。總之,選擇單例模式就是為了避免不一致狀態,避免政出多頭。
二、單例模式特點:
1、單例類只能有一個實例。
2、單例類必須自己創建自己的唯一實例。
3、單例類必須給所有其他對象提供這一實例。
單例模式保證了全局對象的唯一性,比如系統啟動讀取配置文件就需要單例保證配置的一致性。
三、線程安全的問題
一方面在獲取單例的時候,要保證不能產生多個實例對象,后面會詳細講到五種實現方式;
另一方面,在使用單例對象的時候,要注意單例對象內的實例變量是會被多線程共享的,推薦使用無狀態的對象,不會因為多個線程的交替調度而破壞自身狀態導致線程安全問題,比如我們常用的VO,DTO等(局部變量是在用戶棧中的,而且用戶棧本身就是線程私有的內存區域,所以不存在線程安全問題)。
四、單例模式的選擇
還記得我們最早使用的MVC框架Struts1中的action就是單例模式的,而到了Struts2就使用了多例。在Struts1里,當有多個請求訪問,每個都會分配一個新線程,在這些線程,操作的都是同一個action對象,每個用戶的數據都是不同的,而action卻只有一個。到了Struts2, action對象為每一個請求產生一個實例,並不會帶來線程安全問題(實際上servlet容器給每個請求產生許多可丟棄的對象,但是並沒有影響到性能和垃圾回收問題,有時間會做下研究)。
五、實現單例模式的方式
1.餓漢式單例(立即加載方式)
// 餓漢式單例
public class Singleton1 {
// 私有構造
private Singleton1() {}
private static Singleton1 single = new Singleton1();
// 靜態工廠方法
public static Singleton1 getInstance() {
return single;
}
}
餓漢式單例在類加載初始化時就創建好一個靜態的對象供外部使用,除非系統重啟,這個對象不會改變,所以本身就是線程安全的。
Singleton通過將構造方法限定為private避免了類在外部被實例化,在同一個虛擬機范圍內,Singleton的唯一實例只能通過getInstance()方法訪問。(事實上,通過Java反射機制是能夠實例化構造方法為private的類的,那基本上會使所有的Java單例實現失效。此問題在此處不做討論,姑且閉着眼就認為反射機制不存在。)
2.懶漢式單例(延遲加載方式)
// 懶漢式單例
public class Singleton2 {
// 私有構造
private Singleton2() {}
private static Singleton2 single = null;
public static Singleton2 getInstance() {
if(single == null){
single = new Singleton2();
}
return single;
}
}
該示例雖然用延遲加載方式實現了懶漢式單例,但在多線程環境下會產生多個single對象,如何改造請看以下方式:
使用synchronized同步鎖
public class Singleton3 {
// 私有構造
private Singleton3() {}
private static Singleton3 single = null;
public static Singleton3 getInstance() {
// 等同於 synchronized public static Singleton3 getInstance()
synchronized(Singleton3.class){
// 注意:里面的判斷是一定要加的,否則出現線程安全問題
if(single == null){
single = new Singleton3();
}
}
return single;
}
}
在方法上加synchronized同步鎖或是用同步代碼塊對類加同步鎖,此種方式雖然解決了多個實例對象問題,但是該方式運行效率卻很低下,下一個線程想要獲取對象,就必須等待上一個線程釋放鎖之后,才可以繼續運行。
public class Singleton4 {
// 私有構造
private Singleton4() {}
//重點注意volatile
private static volatile Singleton4 single = null;
// 雙重檢查
public static Singleton4 getInstance() {
if (single == null) { //操作①第一次檢查
synchronized (Singleton4.class) {
if (single == null) { //操作②第二次檢查
single = new Singleton4(); //操作③
}
}
}
return single;
}
}
使用雙重檢查進一步做了優化,可以避免整個方法被鎖,只對需要鎖的代碼部分加鎖,可以提高執行效率。
操作③可以分解為幾個獨立的子操作:
objRef = allocate(Singleton4.class);//子操作①:分配存儲空間
invokeConstructor(objRef);//子操作②:初始化objRef引用的對象
single = objRef;//子操作③:將對象引用寫入共享變量
根據鎖的重排序規則,臨界區內的操作可以在臨界區內被重排。所以,JIN編譯器可能將上述子操作重排為:①->③->②,即在初始化對象之前將對象的引用寫入實例變量single。由於鎖對有序性的保障是有條件的,而操作①(第一次檢查)讀取single變量時沒有加鎖,因此上述重排序對操作①的執行線程是有影響的:該線程可能看到一個未初始化(或未初始化完成)的實例,即變量single的值不是null,但是該變量所引用的對象中的某些實例變量的變量值可能仍為默認值,而不是構造器中設置的初始值。也就是說,一個線程在執行操作①的時候發現single不為null,於是該線程就直接返回這個single變量鎖引用的實例,而這個實例可能是未初始化完成的,這就可能導致錯誤。
解決:如上的重點注意,加上volatile關鍵字。
3.靜態內部類實現
public class Singleton6 {
// 私有構造
private Singleton6() {}
// 靜態內部類
private static class InnerObject{
private static Singleton6 single = new Singleton6();
}
public static Singleton6 getInstance() {
return InnerObject.single;
}
}
靜態內部類雖然保證了單例在多線程並發下的線程安全性,但是在遇到序列化對象時,默認的方式運行得到的結果就是多例的。這種情況不多做說明了,使用時請注意。
4.static靜態代碼塊實現
public class Singleton6 {
// 私有構造
private Singleton6() {}
private static Singleton6 single = null;
// 靜態代碼塊
static{
single = new Singleton6();
}
public static Singleton6 getInstance() {
return single;
}
}
5.內部枚舉類實現
public class SingletonFactory {
// 內部枚舉類
private enum EnmuSingleton{
Singleton;
private Singleton8 singleton;
//枚舉類的構造方法在類加載是被實例化
private EnmuSingleton(){
singleton = new Singleton8();
}
public Singleton8 getInstance(){
return singleton;
}
}
public static Singleton8 getInstance() {
return EnmuSingleton.Singleton.getInstance();
}
}
class Singleton8{
public Singleton8(){}
}
以上就是本文要介紹的所有單例模式的理解和實現,相信這篇文章能讓大家更清楚的理解單例模式,希望大家有問題可以探討,多多指教!
備注:本文單例實現部分,實例源碼參照《Java多線程編程核心技術》-(高洪岩)一書中第六章的學習案例撰寫。
《java多線程編程實戰指南 核心篇》 - (黃文海)