設計模式】單例設計模式

1.單例模式的定義
單例模式確保某個類只有一個實例,而且自行實例化並向整個系統提供這個實例。

2.單例模式的特點
單例類只能有一個實例。
單例類必須自己創建自己的唯一實例。
單例類必須給所有其他對象提供這一實例。
3.單例模式的應用
在計算機系統中,線程池、緩存、日誌對象、對話框、打印機、顯卡的驅動程序對象常被設計成單例。

這些應用都或多或少具有資源管理器的功能。每臺計算機可以有若干個打印機,但只能有一個Printer Spooler,以避免兩個打印作業同時輸出到打印機中。每臺計算機可以有若干通信端口,系統應當集中管理這些通信端口,以避免一個通信端口同時被兩個請求同時調用。總之,選擇單例模式就是爲了避免不一致狀態。

4.單例模式的Java代碼
單例模式分爲懶漢式(需要纔去創建對象)和餓漢式(創建類的實例時就去創建對象)。

5.餓漢式
屬性實例化對象
//餓漢模式:線程安全,耗費資源。
public class HugerSingletonTest {
//該對象的引用不可修改
private static final HugerSingletonTest ourInstance = new HugerSingletonTest();

public static HugerSingletonTest getInstance() {
    return ourInstance;
}

private HugerSingletonTest() {}

}

在靜態代碼塊實例對象

public class Singleton {
private static Singleton ourInstance;

static {
     ourInstance = new Singleton();
}

public static Singleton getInstance() {
    return ourInstance;
}

private Singleton() {}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
分析:餓漢式單例模式只要調用了該類,就會實例化一個對象,但有時我們並只需要調用該類中的一個方法,而不需要實例化一個對象,所以餓漢式是比較消耗資源的。

6.懶漢式
非線程安全
public class Singleton {
private static Singleton ourInstance;

public static Singleton getInstance() {
    if (null == ourInstance) {
        ourInstance = new Singleton();
    }
    return ourInstance;
}

private Singleton() {}

}
1
2
3
4
5
6
7
8
9
10
11
12
分析:如果有兩個線程同時調用getInstance()方法,則會創建兩個實例化對象。所以是非線程安全的。

線程安全:給方法加鎖
public class Singleton {
private static Singleton ourInstance;

public synchronized static Singleton getInstance() {
    if (null == ourInstance) {
        ourInstance = new Singleton();
    }
    return ourInstance;
}

private Singleton() {}

}
1
2
3
4
5
6
7
8
9
10
11
12
分析:如果有多個線程調用getInstance()方法,當一個線程獲取該方法,而其它線程必須等待,消耗資源。

線程安全:雙重檢查鎖(同步代碼塊)
public class Singleton {
private static Singleton ourInstance;

public static Singleton getInstance() {
    if (null == ourInstance) {
        synchronized (Singleton.class) {
            if (null == ourInstance) {
                ourInstance = new Singleton();
            }
        }
    }
    return ourInstance;
}

private Singleton() {}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
分析:爲什麼需要雙重檢查鎖呢?因爲第一次檢查是確保之前是一個空對象,而非空對象就不需要同步了,空對象的線程然後進入同步代碼塊,如果不加第二次空對象檢查,兩個線程同時獲取同步代碼塊,一個線程進入同步代碼塊,另一個線程就會等待,而這兩個線程就會創建兩個實例化對象,所以需要在線程進入同步代碼塊後再次進行空對象檢查,才能確保只創建一個實例化對象。

線程安全:靜態內部類
public class Singleton {
private static class SingletonHodler {
private static final Singleton ourInstance = new Singleton();
}

public static Singleton getInstance() {
    return SingletonHodler.ourInstance;
}

private Singleton() {}

}
1
2
3
4
5
6
7
8
9
10
11
分析:利用靜態內部類,某個線程在調用該方法時會創建一個實例化對象。

線程安全:枚舉
enum SingletonTest {
INSTANCE;
public void whateverMethod() {

}

}
1
2
3
4
5
6
分析:枚舉的方式是《Effective Java》書中提倡的方式,它不僅能避免多線程同步問題,而且還能防止反序列化重新創建新的對象,但是在枚舉中的其他任何方法的線程安全由程序員自己負責。還有防止上面的通過反射機制調用私用構造器。不過,由於Java1.5中才加入enum特性,所以使用的人並不多。

線程安全:使用ThreadLocal
public class Singleton {
private static final ThreadLocal tlSingleton =
new ThreadLocal() {
@Override
protected Singleton initialValue() {
return new Singleton();
}
};

public static Singleton getInstance() {
    return tlSingleton.get();
}

private Singleton() {}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
分析:ThreadLocal會爲每一個線程提供一個獨立的變量副本,從而隔離了多個線程對數據的訪問衝突。對於多線程資源共享的問題,同步機制採用了“以時間換空間”的方式,而ThreadLocal採用了“以空間換時間”的方式。前者僅提供一份變量,讓不同的線程排隊訪問,而後者爲每一個線程都提供了一份變量,因此可以同時訪問而互不影響。

線程安全:CAS鎖
public class Singleton {
private static final AtomicReference INSTANCE = new AtomicReference<>();

/**
 * 用CAS確保線程安全
 */
public static Singleton getInstance() {
    while (true) {
        Singleton current = INSTANCE.get();
        if (current != null) {
            return current;
        }
        current = new Singleton();
        if (INSTANCE.compareAndSet(null, current)) {
            return current;
        }
    }
}

private Singleton() {}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
7.指令重排序
我們再來思考一個問題,就是懶漢式的雙重檢查版本的單例模式,它一定是線程安全的嗎?我會毫不猶豫的告訴你—不一定,因爲在JVM的編譯過程中會存在指令重排序的問題。

其實創建一個對象,往往包含三個過程。
對於singleton = new Singleton(),這不是一個原子操作,在 JVM 中包含的三個過程。

1>給 singleton 分配內存

2>調用 Singleton 的構造函數來初始化成員變量,形成實例

3>將singleton對象指向分配的內存空間(執行完這步 singleton纔是非 null 了)

但是,由於JVM會進行指令重排序,所以上面的第二步和第三步的順序是不能保證的,最終的執行順序可能是 1-2-3 也可能是 1-3-2。如果是 1-3-2,則在 3 執行完畢、2 未執行之前,被l另一個線程搶佔了,這時 instance 已經是非 null 了(但卻沒有初始化),所以這個線程會直接返回 instance,然後使用,那肯定就會報錯了。

針對這種情況,我們有什麼解決方法呢?那就是把singleton聲明成 volatile ,改進後的懶漢式線程安全(雙重檢查鎖)的代碼如下:

public class Singleton {
//volatile的作用是:保證可見性、禁止指令重排序,但不能保證原子性
private volatile static Singleton ourInstance;

public static Singleton getInstance() {
    if (null == ourInstance) {
        synchronized (Singleton.class) {
            if (null == ourInstance) {
                ourInstance = new Singleton();
            }
        }
    }
    return ourInstance;
}

private Singleton() {
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
8.單例模式在JDK8源碼中的使用
當然JDK源碼中使用了大量的設計模式,那哪些地方使用了單例設計模式呢?

Runtime類部分源碼如下
//餓漢式單例設計模式
public class Runtime {
private static Runtime currentRuntime = new Runtime();

public static Runtime getRuntime() {
    return currentRuntime;
}

private Runtime() {
}

//省略很多行

}
1
2
3
4
5
6
7
8
9
10
11
12
13

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