單例模式是 Java 中最簡單的設計模式之一,它是指一個類在運行期間始終只有一個實體,我們就把它稱之為單例模式,它不但被應用在實際的作業中,而且還是面試中最常考的題目之一,通過單例模式我們可以知道此人的編程風格,以及對于基礎知識的掌握是否牢固,
我們本課時的面試題是,單例的實作方式有幾種?它們有什么優缺點?
典型回答
單例的實作分為餓漢模式和懶漢模式,顧名思義,餓漢模式就好比他是一個餓漢,而且有一定的危機意識,他會提前把食物囤積好,以備餓了之后直接能吃到食物,對應到程式中指的是,在類加載時就會進行單例的初始化,以后訪問時直接使用單例物件即可,
餓漢模式的實作代碼如下:
public class Singleton {
// 宣告私有物件
private static Singleton instance = new Singleton();
// 獲取實體(單例物件)
public static Singleton getInstance() {
return instance;
}
private Singleton() {
}
// 方法
public void sayHi() {
System.out.println("Hi,Java.");
}
}
class SingletonTest {
public static void main(String[] args) {
// 呼叫單例物件
Singleton singleton = Singleton.getInstance();
// 呼叫方法
singleton.sayHi();
}
}
以上程式的執行結果為:
Hi,Java.
從上述結果可以看出,單例物件已經被成功獲取到并順利地執行了類中的方法,它的優點是執行緒安全,因為單例物件在類加載的時候就已經被初始化了,當呼叫單例物件時只是把早已經創建好的物件賦值給變數;它的缺點是可能會造成資源浪費,如果類加載了單例物件(物件被創建了),但是一直沒有使用,這樣就造成了資源的浪費,
懶漢模式也被稱作為飽漢模式,顧名思義他比較懶,每次只有需要吃飯的時候,才出去找飯吃,而不是像餓漢那樣早早把飯準備好,對應到程式中指的是,當每次需要使用實體時,再去創建獲取實體,而不是在類加載時就將實體創建好,
懶漢模式的實作代碼如下:
public class Singleton {
// 宣告私有物件
private static Singleton instance;
// 獲取實體(單例物件)
public static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
private Singleton() {
}
// 方法
public void sayHi() {
System.out.println("Hi,Java.");
}
}
class SingletonTest {
public static void main(String[] args) {
Singleton singleton = Singleton.getInstance();
singleton.sayHi();
}
}
以上程式的執行結果為:
Hi,Java.
從上述結果可以看出,單例物件已經被成功獲取到并順利地執行了類中的方法,它的優點是不會造成資源的浪費,因為在呼叫的時候才會創建被實體化物件;它的缺點在多執行緒環境下是非執行緒是安全的,比如多個執行緒同時執行到 if 判斷處,此時判斷結果都是未被初始化,那么這些執行緒就會同時創建 n 個實體,這樣就會導致意外的情況發生,
考點分析
使用單例模式可以減少系統的記憶體開銷,提高程式的運行效率,但是使用不當的話就會造成多執行緒下的并發問題,餓漢模式為最直接的實作單例模式的方法,但它可能會造成對系統資源的浪費,所以只有既能保證執行緒安全,又可以避免系統資源被浪費的回答才能徹底地征服面試官,
和此知識點相關的面試題還有以下這些:
- 什么是雙重檢測鎖?它是執行緒安全的嗎?
- 單例的還有其他實作方式嗎?
知識擴展
雙重檢測鎖
為了保證懶漢模式的執行緒安全我們最簡單的做法就是給獲取實體的方法上加上 synchronized(同步鎖)修飾,如下代碼所示:
public class Singleton {
// 宣告私有物件
private static Singleton instance;
// 獲取實體(單例物件)
public synchronized static Singleton getInstance() {
if (instance == null) {
instance = new Singleton();
}
return instance;
}
private Singleton() {
}
// 類方法
public void sayHi() {
System.out.println("Hi,Java.");
}
}
這樣雖然能讓懶漢模式變成執行緒安全的,但由于整個方法都被 synchronized 所包圍,因此增加了同步開銷,降低了程式的執行效率,
于是為了改行程式的執行效率,我們將 synchronized 放入到方法中,以此來減少被同步鎖所修飾的代碼范圍,實作代碼如下:
public class Singleton {
// 宣告私有物件
private static Singleton instance;
// 獲取實體(單例物件)
public static Singleton getInstance() {
if (instance == null) {
synchronized (Singleton.class) {
instance = new Singleton();
}
}
return instance;
}
private Singleton() {
}
// 類方法
public void sayHi() {
System.out.println("Hi,Java.");
}
}
細心的你可能會發現以上的代碼也存在著非執行緒安全的問題,例如,當兩個執行緒同時執行到「if (instance == null) { 」判斷時,判斷的結果都為 true,于是他們就排隊都創建了新的物件,這顯然不符合我們的預期,于是就誕生了大名鼎鼎的雙重檢測鎖(Double Checked Lock,DCL),實作代碼如下:
public class Singleton {
// 宣告私有物件
private static Singleton instance;
// 獲取實體(單例物件)
public static Singleton getInstance() {
// 第一次判斷
if (instance == null) {
synchronized (Singleton.class) {
// 第二次判斷
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
private Singleton() {
}
// 類方法
public void sayHi() {
System.out.println("Hi,Java.");
}
}
上述代碼看似完美,其實隱藏著一個不容易被人發現的小問題,該問題就出在 new 物件這行代碼上,也就是 instance = new Singleton() 這行代碼,這行代碼看似是一個原子操作,然而并不是,這行代碼最侄訓被編譯成多潭訓編指令,它大致的執行流程為以下三個步驟:
- 給物件實體分配記憶體空間;
- 呼叫物件的構造方法、初始化成員欄位;
- 將 instance 物件指向分配的記憶體空間,
但由于 CPU 的優化會對執行指令進行重排序,也就說上面的執行流程的執行順序有可能是 1-2-3,也有可能是 1-3-2,假如執行的順序是 1-3-2,那么當 A 執行緒執行到步驟 3 時,切換至 B 執行緒了,而此時 B 執行緒判斷 instance 物件已經指向了對應的記憶體空間,并非為 null 時就會直接進行回傳,而此時因為沒有執行步驟 2,因此得到的是一個未初始化完成的物件,這樣就導致了問題的誕生,執行時間節點如下表所示:
| 時間點 | 執行緒 | 執行操作 |
|---|---|---|
| t1 | A | instance = new Singleton() 的 1-3 步驟,待執行步驟 2 |
| t2 | B | if (instance == null) { 判斷結果為 false |
| t3 | B | 回傳半初始的 instance 物件 |
為了解決此問題,我們可以使用關鍵字 volatile 來修飾 instance 物件,這樣就可以防止 CPU 指令重排,從而完美地運行懶漢模式,實作代碼如下:
public class Singleton {
// 宣告私有物件
private volatile static Singleton instance;
// 獲取實體(單例物件)
public static Singleton getInstance() {
// 第一次判斷
if (instance == null) {
synchronized (Singleton.class) {
// 第二次判斷
if (instance == null) {
instance = new Singleton();
}
}
}
return instance;
}
private Singleton() {
}
// 類方法
public void sayHi() {
System.out.println("Hi,Java.");
}
}
單例其他實作方式
除了以上的 6 種方式可以實作單例模式外,還可以使用靜態內部類和列舉類來實作單例,靜態內部類的實作代碼如下:
public class Singleton {
// 靜態內部類
private static class SingletonInstance {
private static final Singleton instance = new Singleton();
}
// 獲取實體(單例物件)
public static Singleton getInstance() {
return SingletonInstance.instance;
}
private Singleton() {
}
// 類方法
public void sayHi() {
System.out.println("Hi,Java.");
}
}
從上述代碼可以看出,靜態內部類和餓漢方式有異曲同工之妙,它們都采用了類裝載的機制來保證,當初始化實體時只有一個執行緒執行,從而保證了多執行緒下的安全操作,JVM 會在類初始化階段(也就是類裝載階段)創建一個鎖,該鎖可以保證多個執行緒同步執行類初始化的作業,因此在多執行緒環境下,類加載機制依然是執行緒安全的,
但靜態內部類和餓漢方式也有著細微的差別,餓漢方式是在程式啟動時就會進行加載,因此可能造成資源的浪費;而靜態內部類只有在呼叫 getInstance() 方法時,才會裝載內部類從而完成實體的初始化作業,因此不會造成資源浪費的問題,由此可知,此方式也是較為推薦的單例實作方式,
單例的另一種實作方式為列舉,它也是《Effective Java》作者極力推薦地單例實作方式,因為列舉的實作方式不僅是執行緒安全的,而且只會裝載一次,無論是序列化、反序列化、反射還是克隆都不會新創建物件,它的實作代碼如下:
public class Singleton {
// 列舉型別是執行緒安全的,并且只會裝載一次
private enum SingletonEnum {
INSTANCE;
// 宣告單例物件
private final Singleton instance;
// 實體化
SingletonEnum() {
instance = new Singleton();
}
private Singleton getInstance() {
return instance;
}
}
// 獲取實體(單例物件)
public static Singleton getInstance() {
return SingletonEnum.INSTANCE.getInstance();
}
private Singleton() {
}
// 類方法
public void sayHi() {
System.out.println("Hi,Java.");
}
}
class SingletonTest {
public static void main(String[] args) {
Singleton singleton = Singleton.getInstance();
singleton.sayHi();
}
}
以上程式的執行結果為:
Hi,Java.
小結
本課時我們講了 8 種實作單例的方式,包括執行緒安全但可能會造成系統資源浪費的餓漢模式,以及懶漢模式和懶漢模式變種的 5 種實作方式,其中包含了兩種雙重檢測鎖的懶漢變種模式,還有最后兩種執行緒安全且可以實作延遲加載的靜態內部類的實作方式和列舉類的實作方式,其中比較推薦使用的是后兩種單例模式的實作方式,
轉載請註明出處,本文鏈接:https://www.uj5u.com/ruanti/228936.html
標籤:其他
上一篇:面向物件基本概念2-核心
