單例模式對於頻繁使用的對象,可以省略創建對象所花費的時間,這對於那些重量級對象而言,是非常可觀的一筆系統開銷,另外由於
new 操作的次數減少,因而對系統內存的使用頻率也會降低,這將減輕 GC 壓力,縮短 GC 停頓時間。常應用於讀取配置文件,sessionFactory等場景。
非同步的單例模式代碼
public class Singleton implements Runnable{
private static Singleton instance = new Singleton();
public static Singleton getInsatnce(){
return instance;
}
}
雖然實現了單例模式,但並沒有延遲加載的效果
延遲加載的單例模式代碼
public class LazySingleton {
private static LazySingleton instance = null;
public static synchronized LazySingleton getInstance(){
if(instance == null){
instance = new LazySingleton();
}
return instance;
}
}
由於引入了同步關鍵字,導致多線程環境下耗時明顯增加,並且99%的情況下不需要同步的。
Double
Checked locking 模式
public class Singleton{
private static Singleton instance = null;
public static Singleton getInstance(Context context) {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return sInst;
}
}
這段代碼看起來很完美,很可惜,它是有問題。主要在於instance = new Singleton()這句,這並非是一個原子操作,事實上在 JVM 中這句話大概做了下面 3 件事情。
1.給 instance 分配內存
2.調用 Singleton 的構造函數來初始化成員變量
3.將instance對象指向分配的內存空間(執行完這步 instance 就爲非 null 了)
但是在 JVM 的即時編譯器中存在指令重排序的優化。也就是說上面的第二步和第三步的順序是不能保證的,最終的執行順序可能是 1-2-3 也可能是 1-3-2。如果是後者,則在 3 執行完畢、2 未執行之前,被線程二搶佔了,這時 instance 已經是非 null 了(但卻沒有初始化),所以線程二會直接返回 instance,然後使用,然後順理成章地報錯。
我們只需要將 instance 變量聲明成 volatile 就可以了。
public class Singleton{
private volatile static Singleton instance = null;
public static Singleton getInstance(Context context) {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return sInst;
}
}
有些人認爲使用 volatile 的原因是可見性,也就是可以保證線程在本地不會存有 instance 的副本,每次都是去主內存中讀取。但其實是不對的。使用 volatile 的主要原因是其另一個特性:禁止指令重排序優化。也就是說,在 volatile 變量的賦值操作後面會有一個內存屏障(生成的彙編代碼上),讀操作不會被重排序到內存屏障之前。比如上面的例子,取操作必須在執行完
1-2-3 之後或者 1-3-2 之後,不存在執行到 1-3 然後取到值的情況。從「先行發生原則」的角度理解的話,就是對於一個 volatile 變量的寫操作都先行發生於後面對這個變量的讀操作(這裏的“後面”是時間上的先後順序)。