如何創建完美的單例模式?
設計模式在軟件開發者中十分受歡迎。設計模式是對于常見軟件問題的良好解決方案。單例模式是 Java 中 創建型設計模式 的一種。
單例模式的目的是什么?
單例類的目的是控制對象創建,約束對象的數量有且只有一個。單例模式只允許有一個入口來創建類實例。
因為只有一個單例類實例,任何單例類的實例都將只會產生一個類,就像靜態域一樣。當你需要控制資源的時候,例如在數據庫連接或者使用 sockets ,單例模式是非常有用的。
這看起來是一個很簡單的設計模式,但是當我們真正去實現的時候,會帶來許多的實現問題。單例模式的實現在開發者當中總是存在一定爭議。現在,我們將會討論一下如何創建一個單例類以完成下列目的:
限制類的實例并且保證在 JVM 中只存在一個類實例。
讓我們在 Java 中創建單例類并在不同的情況下進行測試。
創建單例類
為了實現單例類,最簡單方法是把構造器變為 private。有兩種初始化方法。
餓漢式
餓漢式初始化,單例類的實例在類加載時被創建,這是創建單例類最簡單的方法。
通過將構造器聲明為 private ,不允許其他類來創建單例類實例。取而代之的是,創建一個靜態方法(通常命名為 getInstance )來提供創建類實例的唯一入口。
public class SingletonClass {
private static volatile SingletonClass sSoleInstance = new SingletonClass();
//private constructor.
private SingletonClass(){}
public static SingletonClass getInstance() {
return sSoleInstance;
}
}
這種方法有一個缺陷,即使在程序沒有使用到它的時候,實例已經被創建了。當你創建數據庫連接或者 socket 時,這可能成為一個相當大的問題,會導致內存泄漏問題。解決方法是當需要的時候再創建實例,我們稱之為懶漢式初始化。
懶漢式
與餓漢式相反,你在 getInstance() 方法中初始化類實例。方法中將會判斷類實例是否已經創建,如果已經存在,將返回舊的實例,反之在 JVM 中創建新的實例并返回。
public class SingletonClass {
private static SingletonClass sSoleInstance;
private SingletonClass(){} //private constructor.
public static SingletonClass getInstance(){
if (sSoleInstance == null){ //if there is no instance available... create new one
sSoleInstance = new SingletonClass();
}
return sSoleInstance;
}
}
我們都知道在 Java 中,如果兩個對象是相同的,那么他們的 hashCode 也是相同的。讓我們測試一下,如果上面的單例類都正確實現,那么將會返回同樣的哈希。
public class SingletonTester {
public static void main(String[] args) {
//Instance 1
SingletonClass instance1 = SingletonClass.getInstance();
//Instance 2
SingletonClass instance2 = SingletonClass.getInstance();
//now lets check the hash key.
System.out.println("Instance 1 hash:" + instance1.hashCode());
System.out.println("Instance 2 hash:" + instance2.hashCode());
}
}
下面是輸出日志:
15:04:341 I/System.out: Instance 1 hash:247127865
15:04:342 I/System.out: Instance 2 hash:247127865
可以看到兩個實例擁有同樣的 hashCode。所以,這就意味著上面的代碼創建了完美的單例類,是嗎?不。
讓單例類反射安全
在上面的單例類中,通過反射可以創建不止一個實例。 Java Reflection 是一個在運行時檢測或者修改類的運行時行為的過程。通過在運行時修改構造器的可見性并通過構造器創建實例可以產生新的單例類實例。運行下面的代碼,單例類還存在嗎?
public class SingletonTester {
public static void main(String[] args) {
//Create the 1st instance
SingletonClass instance1 = SingletonClass.getInstance();
//Create 2nd instance using Java Reflection API.
SingletonClass instance2 = null;
try {
Class<SingletonClass> clazz = SingletonClass.class;
Constructor<SingletonClass> cons = clazz.getDeclaredConstructor();
cons.setAccessible(true);
instance2 = cons.newInstance();
} catch (NoSuchMethodException | InvocationTargetException | IllegalAccessException | InstantiationException e) {
e.printStackTrace();
}
//now lets check the hash key.
System.out.println("Instance 1 hash:" + instance1.hashCode());
System.out.println("Instance 2 hash:" + instance2.hashCode());
}
}
下面是輸出日志:
15:21:48.216 I/System.out: Instance 1 hash:51110277
15:21:48.216 I/System.out: Instance 2 hash:212057050
每一個實例都有不同的 hashCode。顯然這個單例類無法通過測試。
解決方案:
為了預防反射導致的單例失敗,當構造器已經初始化并且其他類再次初始化時,拋出一個運行時異常。讓我們更新 SingletonClass.java 。
public class SingletonClass {
private static SingletonClass sSoleInstance;
//private constructor.
private SingletonClass(){
//Prevent form the reflection api.
if (sSoleInstance != null){
throw new RuntimeException("Use getInstance() method to get the single instance of this class.");
}
}
public static SingletonClass getInstance(){
if (sSoleInstance == null){ //if there is no instance available... create new one
sSoleInstance = new SingletonClass();
}
return sSoleInstance;
}
}
讓單例類線程安全
如果兩個線程幾乎同時嘗試初始化單例類,將會發生什么?讓我們測試下面的代碼,兩個線程幾乎同時被創建并且調用 getInstance() 。
public class SingletonTester {
public static void main(String[] args) {
//Thread 1
Thread t1 = new Thread(new Runnable() {
@Override
public void run() {
SingletonClass instance1 = SingletonClass.getInstance();
System.out.println("Instance 1 hash:" + instance1.hashCode());
}
});
//Thread 2
Thread t2 = new Thread(new Runnable() {
@Override
public void run() {
SingletonClass instance2 = SingletonClass.getInstance();
System.out.println("Instance 2 hash:" + instance2.hashCode());
}
});
//start both the threads
t1.start();
t2.start();
}
}
如果你多次運行這些代碼,有時你會發現不同的線程創建了不同的實例。
16:16:24.148 I/System.out: Instance 1 hash:247127865
16:16:24.148 I/System.out: Instance 2 hash:267260104
這說明了你的單例類不是線程安全的。所有的線程同時調用 getInstance() 方法, sSoleInstance == null 條件對所有線程返回值,所以兩個不同的實例被創建。這打破了單例原則。
解決方案
同步 getInstance() 方法
public class SingletonClass {
private static SingletonClass sSoleInstance;
//private constructor.
private SingletonClass(){
//Prevent form the reflection api.
if (sSoleInstance != null){
throw new RuntimeException("Use getInstance() method to get the single instance of this class.");
}
}
public synchronized static SingletonClass getInstance(){
if (sSoleInstance == null){ //if there is no instance available... create new one
sSoleInstance = new SingletonClass();
}
return sSoleInstance;
}
}
在我們同步 getInstance() 方法之后,第二個線程必須等到第一個線程執行完 getInstance() 方法之后才能執行,這就保證了線程安全。
但是,這個方法同樣有一些缺點:
- 鎖的開銷導致運行變慢
- 實例變量初始化之后的同步操作時不必要的
雙檢查鎖
使用 雙檢查鎖 方法創建實例可以克服上面的問題。
這這種方法中,當實例為空時,在同步代碼塊中創建單例類,這樣只有當 sSoleInstance 為空時,同步代碼塊才會執行,避免了不必要的同步操作。
public class SingletonClass {
private static SingletonClass sSoleInstance;
//private constructor.
private SingletonClass(){
//Prevent form the reflection api.
if (sSoleInstance != null){
throw new RuntimeException("Use getInstance() method to get the single instance of this class.");
}
}
public static SingletonClass getInstance() {
//Double check locking pattern
if (sSoleInstance == null) { //Check for the first time
synchronized (SingletonClass.class) { //Check for the second time.
//if there is no instance available... create new one
if (sSoleInstance == null) sSoleInstance = new SingletonClass();
}
}
return sSoleInstance;
}
}
使用 volatile 關鍵字
表面上看,這個方法看起來很完美,你只需要付出一次靜態代碼塊的代價。但是除非你使用 volatile 關鍵字,否則單例仍然會被打破。
沒有 volatile 修飾符,另一個線程可能在變量 sSoleInstance 正在初始化尚未完成時引用它。但是通過 volatile 的保證 happens-before 關系,所有對于 sSoleInstance 變量的寫操作都會在讀操作之前發生。
public class SingletonClass {
private static volatile SingletonClass sSoleInstance;
//private constructor.
private SingletonClass(){
//Prevent form the reflection api.
if (sSoleInstance != null){
throw new RuntimeException("Use getInstance() method to get the single instance of this class.");
}
}
public static SingletonClass getInstance() {
//Double check locking pattern
if (sSoleInstance == null) { //Check for the first time
synchronized (SingletonClass.class) { //Check for the second time.
//if there is no instance available... create new one
if (sSoleInstance == null) sSoleInstance = new SingletonClass();
}
}
return sSoleInstance;
}
}
現在上面的單例類是線程安全的。在多線程應用環境中(比如安卓應用)保證單例類的線程安全是必需的。
讓單例類序列化安全
在分布式系統中,有些情況下你需要在單例類中實現 Serializable 接口。這樣你可以在文件系統中存儲它的狀態并且在稍后的某一時間點取出。
讓我們測試一個這個單例類在序列化和反序列化之后是否仍然保持單例。
public class SingletonTester {
public static void main(String[] args) {
try {
SingletonClass instance1 = SingletonClass.getInstance();
ObjectOutput out = null;
out = new ObjectOutputStream(new FileOutputStream("filename.ser"));
out.writeObject(instance1);
out.close();
//deserialize from file to object
ObjectInput in = new ObjectInputStream(new FileInputStream("filename.ser"));
SingletonClass instance2 = (SingletonClass) in.readObject();
in.close();
System.out.println("instance1 hashCode=" + instance1.hashCode());
System.out.println("instance2 hashCode=" + instance2.hashCode());
} catch (IOException | ClassNotFoundException e) {
e.printStackTrace();
}
}
}
16:16:24.148 I/System.out: Instance 1 hash:247127865
16:16:24.148 I/System.out: Instance 2 hash:267260104
可以看到實例的 hashCode 是不同的,違反了單例原則。序列化單例類之后,當我們反序列化時,會創建一個新的類實例。為了預防另一個實例的產生,你需要提供 readResolve() 方法的實現。 readResolve() 代替了從流中讀取對象。這就確保了在序列化和反序列化的過程中沒人可以創建新的實例。
public class SingletonClass implements Serializable {
private static volatile SingletonClass sSoleInstance;
//private constructor.
private SingletonClass(){
//Prevent form the reflection api.
if (sSoleInstance != null){
throw new RuntimeException("Use getInstance() method to get the single instance of this class.");
}
}
public static SingletonClass getInstance() {
if (sSoleInstance == null) { //if there is no instance available... create new one
synchronized (SingletonClass.class) {
if (sSoleInstance == null) sSoleInstance = new SingletonClass();
}
}
return sSoleInstance;
}
//Make singleton from serialize and deserialize operation.
protected SingletonClass readResolve() {
return getInstance();
}
}
結論
在文章的最后,你可以創建線程,反射和序列化安全的單例類,但這仍然不是完美的單例,你可以使用克隆或者多個類加載器來創建不止一個實例。但是對于大多數應用,上面的實現方法已經可以很好的工作了。
來自:http://www.jianshu.com/p/4b72c973eeae