單例模式詳解

一直以爲自己對單例模式很瞭解,仔細研究才發現以前忽略了單例模式的很多難點。其中有如何保證在多線程中的單例模式。如何保證反序列化時的單例模式(因爲反序列化時會創建一個新的實例)。

定義:在整個程序中,保證某個類只有一個實例化的對象,並自行對外提供這個實例。
使用場景:在定義中說的很清楚了,只要一個實例的時候。比如IO讀寫時,數據庫操作時。多個對象的話就很有可能造成併發讀寫的操作,也增加了不必要的資源消耗。

  1. 惡漢單例模式
package com.example.singleinstance;

public class HungerSingle {
    private static HungerSingle mHungerSingle = new HungerSingle();
    private HungerSingle(){}

    public static HungerSingle getInstance() {
        return mHungerSingle;
    }

}
  1. 懶漢單例模式
package com.example.singleinstance;

public class LazySingle {
    private static LazySingle mLazySingle = null;
    private LazySingle(){}

    public static LazySingle getInstance() {
        if (mLazySingle == null) {
            mLazySingle = new LazySingle();
        }
        return mLazySingle;
    }

}

上面兩種就是我以前使用的單例模式。兩者的區別在於惡漢在類一聲明的時候就對唯一的對象進行類初始化。而懶漢是在第一次調用getInstance()時進行初始化操作。
之前運行的很好,直到我遇見了多線程。以懶漢爲例,在多線程的操作中,假設A線程已經判斷出對象爲null但還沒有來的及進行實例化操作時,進入了B線程。這時候對象仍然爲null。於是B線程對對象進行了一次實例化。實例化完成後又回到了A線程,這時會對對象再次進行實例化。導致對一個對象進行多次實例化操作。這就違背了單例模式的定義。
保證在多線程中的單例模式
爲此,我需要在多線程時對單例模式進行同步操作。使用synchronized關鍵字。

package com.example.singleinstance;

public class LazySingle {
    private static LazySingle mLazySingle = null;
    private LazySingle(){}

    public static  synchronized LazySingle getInstance() {
        if (mLazySingle == null) {
            mLazySingle = new LazySingle();
        }
        return mLazySingle;
    }

}

然而,這樣會產生一個新的問題。就是在第一次調用getInstance()方法後。這時對象已經不爲null。就是說已經不再需要同步這個方法了。但實際是,調用端在以後的每次調用getInstance()。都會對這個方法進行同步。這就違背了我們多線程的初衷。這時候,我們可以在getInstance()方法裏多加一個判斷。
package com.example.singleinstance;

public class LazySingle {
private static LazySingle mLazySingle = null;
private LazySingle(){}

public static LazySingle getInstance() {
    if (mLazySingle == null) {
        synchronized (LazySingle.class) {
            if (mLazySingle == null) {
                mLazySingle = new LazySingle();
            }
        }

    }
    return mLazySingle;
}

}

這樣做的好處就是,只會在第一次調用getInstance()方法時進行同步操作。以後再調用getInstance()都是多線程的異步操作了。

似乎這樣做就完美解決了多線程的問題。然而並沒有。在特定的情況下,這個程序仍然可能違背單例模式的定義。在說明這個問題前,我先說一下關於java實例化對象時的一個小知識
實例化對象分爲三個步驟:

  1. 爲對象分配一個內存空間。
  2. 初始化這個對象,即初始化這個對象的成員。
  3. 將這個對象指向之前我們分配的內存空間。(這時候這個對象就不是null了)
    很重要的一點是,java編譯器是支持亂序執行的。就是說,java在實例化這個對象時的順序並不是1.2.3.很有可能是1.3.2。現在我們假設是後者。當A線程執行完第一步和第三步時跑到了B線程。這時候這個對象已經不是null了。B線程不對對象進行實例化操作,而是直接調用這個對象裏的成員變量或成員函數。這時候程序就會報錯了。
    爲此,SUN公司在jdk1.5之後加上了volatile關鍵字。
package com.example.singleinstance;

public class LazySingle {
    private static volatile LazySingle mLazySingle = null;
    private LazySingle(){}

    public static LazySingle getInstance() {
        if (mLazySingle == null) {
            synchronized (LazySingle.class) {
                if (mLazySingle == null) {
                    mLazySingle = new LazySingle();
                }
            }

        }
        return mLazySingle;
    }

}

在聲明這個對象的時候,加上volatile關鍵字修飾,就不會出現上述問題了。至於爲什麼,就自己百度吧。

  1. 推薦的單例模式,靜態內部類單例模式。
    說了那麼多,然而被推薦的是現在說的這種單例模式。
package com.example.singleinstance;

public class Singleton {
    private Singleton() {
    }

    public static Singleton getInstance() {
        return SingletonHolder.SINGLETON;
    }

    /**
     * 內部靜態類
     * 
     * @author cyq
     */
    private static class SingletonHolder {
        private static final Singleton SINGLETON = new Singleton();
    }

}

當第一次加載這個類時,並不會初始化唯一的對象,只有在調用getInstance()方法時會導致虛擬機加載內部靜態類SingletonHolder。不僅能保證線程的安全,也保證了單例對象的唯一性。推薦使用。

如何保證反序列化時也是單例模式。
序列化就是讀取時的操作。比如讀取到一個磁盤上。序列化就是將對象存儲到磁盤上的過程,反序列化就是從磁盤上將存儲的對象取出來。注意這時候取的並不是原來的對象。而是和原來一模一樣的一個新的對象。產生了新的對象,就不能保證對象的唯一性了。好在序列化給我們提供了一個很特別的函數readResolv()。通過這個函數,我們可以控制反序列化時返回的對象。

package com.example.singleinstance;

public class Singleton {
    private Singleton() {
    }

    public static Singleton getInstance() {
        return SingletonHolder.SINGLETON;
    }

    /**
     * 內部靜態類
     * 
     * @author cyq
     */
    private static class SingletonHolder {
        private static final Singleton SINGLETON = new Singleton();
    }

    private Object readResolv() {
        return SingletonHolder.SINGLETON;
    }

}

通過上面的講解,發現簡單的單例模式原來那麼複雜。有沒有一種簡單有效的方法。答案是肯定的,那就是通過枚舉
2. 枚舉單例模式(簡單有效)
枚舉的好處是默認線程同步的,而且也不會受到反序列化的影響,而且最重要的是,在任何一種情況下它都是單例的。

package com.example.singleinstance;

public enum SingletonEnum {
    INSTANCE;
    public void doSomething() {
        System.out.println("do sth");
    }

}

使用枚舉單例

package com.example.singleinstance;

import android.app.Activity;
import android.os.Bundle;
import android.view.Menu;
import android.view.MenuItem;

public class MainActivity extends Activity {

    @Override
    protected void onCreate(Bundle savedInstanceState) {
        super.onCreate(savedInstanceState);
        setContentView(R.layout.activity_main);
        SingletonEnum.INSTANCE.doSomething();
    }
}

是不是超簡單。

總結:經過研究發現簡單的一個單例模式如果用的不好,是很有可能發生錯誤的。java編程“博大精深”,只有不斷的學習和積累,使自己的知識更加深入和廣泛,才能從容的面對各種項目上的需求,尋找出最簡單高效的解決方案。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章