我是如何用單例模式吊打面試官的?

前言

單例模式無論在我們面試,還是日常工作中,都會面對的問題。但很多單例模式的細節,值得我們深入探索一下。

這篇文章透過單例模式,串聯了多方面基礎知識,非常值得一讀。

1 什麼是單例模式?

單例模式是一種非常常用的軟體設計模式,它定義是單例對象的類只能允許一個實例存在

該類負責創建自己的對象,同時確保只有一個對象被創建。一般常用在工具類的實現或創建對象需要消耗資源的業務場景。

單例模式的特點:

  • 類構造器私有
  • 持有自己類的引用
  • 對外提供獲取實例的靜態方法

我們先用一個簡單示例了解一下單例模式的用法。

public class SimpleSingleton {
    //持有自己類的引用
    private static final SimpleSingleton INSTANCE = new SimpleSingleton();

    //私有的構造方法
    private SimpleSingleton() {
    }
    //對外提供獲取實例的靜態方法
    public static SimpleSingleton getInstance() {
        return INSTANCE;
    }
    
    public static void main(String[] args) {
        System.out.println(SimpleSingleton.getInstance().hashCode());
        System.out.println(SimpleSingleton.getInstance().hashCode());
    }
}

列印結果:

1639705018
1639705018

我們看到兩次獲取SimpleSingleton實例的hashCode是一樣的,說明兩次調用獲取到的是同一個對象。

可能很多朋友平時工作當中都是這麼用的,但我要說這段程式碼是有問題的,你會相信嗎?

不信,我們一起往下看。

2 餓漢和懶漢模式

在介紹單例模式的時候,必須要先介紹它的兩種非常著名的實現方式:餓漢模式懶漢模式

2.1 餓漢模式

實例在初始化的時候就已經建好了,不管你有沒有用到,先建好了再說。具體程式碼如下:

public class SimpleSingleton {
    //持有自己類的引用
    private static final SimpleSingleton INSTANCE = new SimpleSingleton();

    //私有的構造方法
    private SimpleSingleton() {
    }
    //對外提供獲取實例的靜態方法
    public static SimpleSingleton getInstance() {
        return INSTANCE;
    }
}

餓漢模式,其實還有一個變種:

public class SimpleSingleton {
    //持有自己類的引用
    private static final SimpleSingleton INSTANCE;
    static {
       INSTANCE = new SimpleSingleton();
    }

    //私有的構造方法
    private SimpleSingleton() {
    }
    //對外提供獲取實例的靜態方法
    public static SimpleSingleton getInstance() {
        return INSTANCE;
    }
}

使用靜態程式碼塊的方式實例化INSTANCE對象。

使用餓漢模式的好處是:沒有執行緒安全的問題,但帶來的壞處也很明顯。

private static final SimpleSingleton INSTANCE = new SimpleSingleton();

一開始就實例化對象了,如果實例化過程非常耗時,並且最後這個對象沒有被使用,不是白白造成資源浪費嗎?

還真是啊。

這個時候你也許會想到,不用提前實例化對象,在真正使用的時候再實例化不就可以了?

這就是我接下來要介紹的:懶漢模式

2.2 懶漢模式

顧名思義就是實例在用到的時候才去創建,「比較懶」,用的時候才去檢查有沒有實例,如果有則返回,沒有則新建。具體程式碼如下:

public class SimpleSingleton2 {

    private static SimpleSingleton2 INSTANCE;

    private SimpleSingleton2() {
    }

    public static SimpleSingleton2 getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new SimpleSingleton2();
        }
        return INSTANCE;
    }
}

示例中的INSTANCE對象一開始是空的,在調用getInstance方法才會真正實例化。

嗯,不錯不錯。但這段程式碼還是有問題。

2.3 synchronized關鍵字

上面的程式碼有什麼問題?

答:假如有多個執行緒中都調用了getInstance方法,那麼都走到 if (INSTANCE == null) 判斷時,可能同時成立,因為INSTANCE初始化時默認值是null。這樣會導致多個執行緒中同時創建INSTANCE對象,即INSTANCE對象被創建了多次,違背了只創建一個INSTANCE對象的初衷。

那麼,要如何改進呢?

答:最簡單的辦法就是使用synchronized關鍵字。

改進後的程式碼如下:

public class SimpleSingleton3 {
    private static SimpleSingleton3 INSTANCE;

    private SimpleSingleton3() {
    }

    public synchronized static SimpleSingleton3 getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new SimpleSingleton3();
        }
        return INSTANCE;
    }
    public static void main(String[] args) {
        System.out.println(SimpleSingleton3.getInstance().hashCode());
        System.out.println(SimpleSingleton3.getInstance().hashCode());
    }
}

在getInstance方法上加synchronized關鍵字,保證在並發的情況下,只有一個執行緒能創建INSTANCE對象的實例。

這樣總可以了吧?

答:不好意思,還是有問題。

有什麼問題?

答:使用synchronized關鍵字會消耗getInstance方法的性能,我們應該判斷當INSTANCE為空時才加鎖,如果不為空不應該加鎖,需要直接返回。

這就需要使用下面要說的雙重檢查鎖了。

2.4 餓漢和懶漢模式的區別

but,在介紹雙重檢查鎖之前,先插播一個朋友們可能比較關心的話題:餓漢模式 和 懶漢模式 各有什麼優缺點?

  • 餓漢模式:優點是沒有執行緒安全的問題,缺點是浪費記憶體空間。
  • 懶漢模式:優點是沒有記憶體空間浪費的問題,缺點是如果控制不好,實際上不是單例的。

好了,下面可以安心的看看雙重檢查鎖,是如何保證性能的,同時又保證單例的。

3 雙重檢查鎖

雙重檢查鎖顧名思義會檢查兩次:在加鎖之前檢查一次是否為空,加鎖之後再檢查一次是否為空。

那麼,它是如何實現單例的呢?

3.1 如何實現單例?

具體程式碼如下:

public class SimpleSingleton4 {

    private static SimpleSingleton4 INSTANCE;

    private SimpleSingleton4() {
    }

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

在加鎖之前判斷是否為空,可以確保INSTANCE不為空的情況下,不用加鎖,可以直接返回。

為什麼在加鎖之後,還需要判斷INSTANCE是否為空呢?

答:是為了防止在多執行緒並發的情況下,只會實例化一個對象。

比如:執行緒a和執行緒b同時調用getInstance方法,假如同時判斷INSTANCE都為空,這時會同時進行搶鎖。

假如執行緒a先搶到鎖,開始執行synchronized關鍵字包含的程式碼,此時執行緒b處於等待狀態。

執行緒a創建完新實例了,釋放鎖了,此時執行緒b拿到鎖,進入synchronized關鍵字包含的程式碼,如果沒有再判斷一次INSTANCE是否為空,則可能會重複創建實例。

所以需要在synchronized前後兩次判斷。

不要以為這樣就完了,還有問題呢?

3.2 volatile關鍵字

上面的程式碼還有啥問題?

public static SimpleSingleton4 getInstance() {
      if (INSTANCE == null) {//1
          synchronized (SimpleSingleton4.class) {//2
              if (INSTANCE == null) {//3
                  INSTANCE = new SimpleSingleton4();//4
              }
          }
      }
      return INSTANCE;//5
  }

getInstance方法的這段程式碼,我是按1、2、3、4、5這種順序寫的,希望也按這個順序執行。

但是java虛擬機實際上會做一些優化,對一些程式碼指令進行重排。重排之後的順序可能就變成了:1、3、2、4、5,這樣在多執行緒的情況下同樣會創建多次實例。重排之後的程式碼可能如下:

public static SimpleSingleton4 getInstance() {
    if (INSTANCE == null) {//1
       if (INSTANCE == null) {//3
           synchronized (SimpleSingleton4.class) {//2
                INSTANCE = new SimpleSingleton4();//4
            }
        }
    }
    return INSTANCE;//5
}

原來如此,那有什麼辦法可以解決呢?

答:可以在定義INSTANCE是加上volatile關鍵字。具體程式碼如下:

public class SimpleSingleton7 {

    private volatile static SimpleSingleton7 INSTANCE;

    private SimpleSingleton7() {
    }

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

volatile關鍵字可以保證多個執行緒的可見性,但是不能保證原子性。同時它也能禁止指令重排。

雙重檢查鎖的機制既保證了執行緒安全,又比直接上鎖提高了執行效率,還節省了記憶體空間。

除了上面的單例模式之外,還有沒有其他的單例模式?

4 靜態內部類

靜態內部類顧名思義是通過靜態的內部類來實現單例模式的。

那麼,它是如何實現單例的呢?

4.1 如何實現單例模式?

具體程式碼如下:

public class SimpleSingleton5 {

    private SimpleSingleton5() {
    }

    public static SimpleSingleton5 getInstance() {
        return Inner.INSTANCE;
    }

    private static class Inner {
        private static final SimpleSingleton5 INSTANCE = new SimpleSingleton5();
    }
}

我們看到在SimpleSingleton5類中定義了一個靜態的內部類Inner。在SimpleSingleton5類的getInstance方法中,返回的是內部類Inner的實例INSTANCE對象。

只有在程式第一次調用getInstance方法時,虛擬機才載入Inner並實例化INSTANCE對象。

java內部機制保證了,只有一個執行緒可以獲得對象鎖,其他的執行緒必須等待,保證對象的唯一性。

4.2 反射漏洞

上面的程式碼看似完美,但還是有漏洞。如果其他人使用反射,依然能夠通過類的無參構造方式創建對象。例如:

Class<SimpleSingleton5> simpleSingleton5Class = SimpleSingleton5.class;
try {
    SimpleSingleton5 newInstance = simpleSingleton5Class.newInstance();
    System.out.println(newInstance == SimpleSingleton5.getInstance());
} catch (InstantiationException e) {
    e.printStackTrace();
} catch (IllegalAccessException e) {
    e.printStackTrace();
}

上面程式碼列印結果是false。

由此看出,通過反射創建的對象,跟通過getInstance方法獲取的對象,並非同一個對象,也就是說,這個漏洞會導致SimpleSingleton5非單例。

那麼,要如何防止這個漏洞呢?

答:這就需要在無參構造方式中判斷,如果非空,則拋出異常了。

改造後的程式碼如下:

public class SimpleSingleton5 {

    private SimpleSingleton5() {
        if(Inner.INSTANCE != null) {
           throw new RuntimeException("不能支援重複實例化");
       }
    }

    public static SimpleSingleton5 getInstance() {
        return Inner.INSTANCE;
    }

    private static class Inner {
        private static final SimpleSingleton5 INSTANCE = new SimpleSingleton5();
        }
    }

}

如果此時,你認為這種靜態內部類,實現單例模式的方法,已經完美了。

那麼,我要告訴你的是,你錯了,還有漏洞。。。

4.3 反序列化漏洞

眾所周知,java中的類通過實現Serializable介面,可以實現序列化。

我們可以把類的對象先保存到記憶體,或者某個文件當中。後面在某個時刻,再恢復成原始對象。

具體程式碼如下:

public class SimpleSingleton5 implements Serializable {

    private SimpleSingleton5() {
        if (Inner.INSTANCE != null) {
            throw new RuntimeException("不能支援重複實例化");
        }
    }

    public static SimpleSingleton5 getInstance() {
        return Inner.INSTANCE;
    }

    private static class Inner {
        private static final SimpleSingleton5 INSTANCE = new SimpleSingleton5();
    }

    private static void writeFile() {
        FileOutputStream fos = null;
        ObjectOutputStream oos = null;
        try {
            SimpleSingleton5 simpleSingleton5 = SimpleSingleton5.getInstance();
            fos = new FileOutputStream(new File("test.txt"));
            oos = new ObjectOutputStream(fos);
            oos.writeObject(simpleSingleton5);
            System.out.println(simpleSingleton5.hashCode());
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } finally {
            if (oos != null) {
                try {
                    oos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (fos != null) {
                try {
                    fos.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }

        }
    }

    private static void readFile() {
        FileInputStream fis = null;
        ObjectInputStream ois = null;
        try {
            fis = new FileInputStream(new File("test.txt"));
            ois = new ObjectInputStream(fis);
            SimpleSingleton5 myObject = (SimpleSingleton5) ois.readObject();

            System.out.println(myObject.hashCode());
        } catch (FileNotFoundException e) {
            e.printStackTrace();
        } catch (IOException e) {
            e.printStackTrace();
        } catch (ClassNotFoundException e) {
            e.printStackTrace();
        } finally {
            if (ois != null) {
                try {
                    ois.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
            if (fis != null) {
                try {
                    fis.close();
                } catch (IOException e) {
                    e.printStackTrace();
                }
            }
        }
    }

    public static void main(String[] args) {
        writeFile();
        readFile();
    }
}

運行之後,發現序列化和反序列化後對象的hashCode不一樣:

189568618
793589513

說明,反序列化時創建了一個新對象,打破了單例模式對象唯一性的要求。

那麼,如何解決這個問題呢?

答:重新readResolve方法。

在上面的實例中,增加如下程式碼:

private Object readResolve() throws ObjectStreamException {
    return Inner.INSTANCE;
}

運行結果如下:

290658609
290658609

我們看到序列化和反序列化實例對象的hashCode相同了。

做法很簡單,只需要在readResolve方法中,每次都返回唯一的Inner.INSTANCE對象即可。

程式在反序列化獲取對象時,會去尋找readResolve()方法。

  • 如果該方法不存在,則直接返回新對象。
  • 如果該方法存在,則按該方法的內容返回對象。
  • 如果我們之前沒有實例化單例對象,則會返回null。

好了,到這來終於把坑都踩完了。

還是費了不少勁。

不過,我偷偷告訴你一句,其實還有更簡單的方法,哈哈哈。

納尼。。。

5 枚舉

其實在java中枚舉就是天然的單例,每一個實例只有一個對象,這是java底層內部機制保證的。

簡單的用法:

public enum  SimpleSingleton7 {
    INSTANCE;
    
    public void doSamething() {
        System.out.println("doSamething");
    }
}   

在調用的地方:

public class SimpleSingleton7Test {

    public static void main(String[] args) {
        SimpleSingleton7.INSTANCE.doSamething();
    }
}

在枚舉中實例對象INSTANCE是唯一的,所以它是天然的單例模式。

當然,在枚舉對象唯一性的這個特性,還能創建其他的單例對象,例如:

public enum  SimpleSingleton7 {
    INSTANCE;
    
    private Student instance;
    
    SimpleSingleton7() {
       instance = new Student();
    }
    
    public Student getInstance() {
       return instance;
    }
}

class Student {
}

jvm保證了枚舉是天然的單例,並且不存在執行緒安全問題,此外,還支援序列化。

在java大神Joshua Bloch的經典書籍《Effective Java》中說過:

單元素的枚舉類型已經成為實現Singleton的最佳方法。

6 多例模式

我們之前聊過的單例模式,都只會產生一個實例。但它其實還有一個變種,也就是我們接下來要聊的:多例模式

多例模式顧名思義,它允許創建多個實例。但它的初衷是為了控制實例的個數,其他的跟單例模式差不多。

具體實現程式碼如下:

public class SimpleMultiPattern {
    //持有自己類的引用
    private static final SimpleMultiPattern INSTANCE1 = new SimpleMultiPattern();
    private static final SimpleMultiPattern INSTANCE2 = new SimpleMultiPattern();

    //私有的構造方法
    private SimpleMultiPattern() {
    }
    //對外提供獲取實例的靜態方法
    public static SimpleMultiPattern getInstance(int type) {
        if(type == 1) {
          return INSTANCE1;
        }
        return INSTANCE2;
    }
}

為了看起來更直觀,我把一些額外的安全相關程式碼去掉了。

有些朋友可能會說:既然多例模式也是為了控制實例數量,那我們常見的池技術,比如:資料庫連接池,是不是通過多例模式實現的?

答:不,它是通過享元模式實現的。

那麼,多例模式和享元模式有什麼區別?

  • 多例模式:跟單例模式一樣,純粹是為了控制實例數量,使用這種模式的類,通常是作為程式某個模組的入口。
  • 享元模式:它的側重點是對象之間的銜接。它把動態的、會變化的狀態剝離出來,共享不變的東西。

7 真實使用場景

最後,跟大家一起聊聊,單例模式的一些使用場景。我們主要看看在java的框架中,是如何使用單例模式,給有需要的朋友一個參考。

7.1 Runtime

jdk提供了Runtime類,我們可以通過這個類獲取系統的運行狀態。

比如可以通過它獲取cpu核數:

int availableProcessors = Runtime.getRuntime().availableProcessors();

Runtime類的關鍵程式碼如下:

public class Runtime {
    private static Runtime currentRuntime = new Runtime();
    
    public static Runtime getRuntime() {
        return currentRuntime;
    }

    private Runtime() {}
    ...
}

從上面的程式碼我們可以看出,這確實是一個單例模式,並且是餓漢模式。

但根據文章之前講過的一些理論知識,你會發現Runtime類的這種單例模式實現方式,顯然不太好。實例對象既沒用final關鍵字修飾,也沒考慮對象實例化的性能消耗問題。

不過它的優點是實現起來非常簡單。

7.2 NamespaceHandlerResolver

spring提供的DefaultNamespaceHandlerResolver是為需要初始化默認命名空間處理器,是為了方便後面做標籤解析用的。

它的關鍵程式碼如下:

@Nullable
private volatile Map<String, Object> handlerMappings;

private Map<String, Object> getHandlerMappings() {
		Map<String, Object> handlerMappings = this.handlerMappings;
		if (handlerMappings == null) {
			synchronized (this) {
				handlerMappings = this.handlerMappings;
				if (handlerMappings == null) {
					if (logger.isDebugEnabled()) {
						logger.debug("Loading NamespaceHandler mappings from [" + this.handlerMappingsLocation + "]");
					}
					try {
						Properties mappings =
								PropertiesLoaderUtils.loadAllProperties(this.handlerMappingsLocation, this.classLoader);
						if (logger.isDebugEnabled()) {
							logger.debug("Loaded NamespaceHandler mappings: " + mappings);
						}
						handlerMappings = new ConcurrentHashMap<>(mappings.size());
						CollectionUtils.mergePropertiesIntoMap(mappings, handlerMappings);
						this.handlerMappings = handlerMappings;
					}
					catch (IOException ex) {
						throw new IllegalStateException(
								"Unable to load NamespaceHandler mappings from location [" + this.handlerMappingsLocation + "]", ex);
					}
				}
			}
		}
		return handlerMappings;
	}

我們看到它使用了雙重檢測鎖,並且還定義了一個局部變數handlerMappings,這是非常高明之處。

使用局部變數相對於不使用局部變數,可以提高性能。主要是由於 volatile 變數創建對象時需要禁止指令重排序,需要一些額外的操作。

7.3 LogFactory

mybatis提供LogFactory類是為了創建日誌對象,根據引入的jar包,決定使用哪種方式列印日誌。具體程式碼如下:

public final class LogFactory {

  public static final String MARKER = "MYBATIS";

  private static Constructor<? extends Log> logConstructor;

  static {
    tryImplementation(new Runnable() {
      @Override
      public void run() {
        useSlf4jLogging();
      }
    });
    tryImplementation(new Runnable() {
      @Override
      public void run() {
        useCommonsLogging();
      }
    });
    tryImplementation(new Runnable() {
      @Override
      public void run() {
        useLog4J2Logging();
      }
    });
    tryImplementation(new Runnable() {
      @Override
      public void run() {
        useLog4JLogging();
      }
    });
    tryImplementation(new Runnable() {
      @Override
      public void run() {
        useJdkLogging();
      }
    });
    tryImplementation(new Runnable() {
      @Override
      public void run() {
        useNoLogging();
      }
    });
  }

  private LogFactory() {
    // disable construction
  }

  public static Log getLog(Class<?> aClass) {
    return getLog(aClass.getName());
  }

  public static Log getLog(String logger) {
    try {
      return logConstructor.newInstance(logger);
    } catch (Throwable t) {
      throw new LogException("Error creating logger for logger " + logger + ".  Cause: " + t, t);
    }
  }

  public static synchronized void useCustomLogging(Class<? extends Log> clazz) {
    setImplementation(clazz);
  }

  public static synchronized void useSlf4jLogging() {
    setImplementation(org.apache.ibatis.logging.slf4j.Slf4jImpl.class);
  }

  public static synchronized void useCommonsLogging() {
    setImplementation(org.apache.ibatis.logging.commons.JakartaCommonsLoggingImpl.class);
  }

  public static synchronized void useLog4JLogging() {
    setImplementation(org.apache.ibatis.logging.log4j.Log4jImpl.class);
  }

  public static synchronized void useLog4J2Logging() {
    setImplementation(org.apache.ibatis.logging.log4j2.Log4j2Impl.class);
  }

  public static synchronized void useJdkLogging() {
    setImplementation(org.apache.ibatis.logging.jdk14.Jdk14LoggingImpl.class);
  }

  public static synchronized void useStdOutLogging() {
    setImplementation(org.apache.ibatis.logging.stdout.StdOutImpl.class);
  }

  public static synchronized void useNoLogging() {
    setImplementation(org.apache.ibatis.logging.nologging.NoLoggingImpl.class);
  }

  private static void tryImplementation(Runnable runnable) {
    if (logConstructor == null) {
      try {
        runnable.run();
      } catch (Throwable t) {
        // ignore
      }
    }
  }

  private static void setImplementation(Class<? extends Log> implClass) {
    try {
      Constructor<? extends Log> candidate = implClass.getConstructor(String.class);
      Log log = candidate.newInstance(LogFactory.class.getName());
      if (log.isDebugEnabled()) {
        log.debug("Logging initialized using '" + implClass + "' adapter.");
      }
      logConstructor = candidate;
    } catch (Throwable t) {
      throw new LogException("Error setting Log implementation.  Cause: " + t, t);
    }
  }
}

這段程式碼非常經典,但它卻是一個不走尋常路的單例模式。因為它創建的實例對象,可能存在多種情況,根據引入不同的jar包,載入不同的類創建實例對象。如果有一個創建成功,則用它作為整個類的實例對象。

這裡有個非常巧妙的地方是:使用了很多tryImplementation方法,方便後面進行擴展。不然要寫很多,又臭又長的if…else判斷。

此外,它跟常規的單例模式的區別是,LogFactory類中定義的實例對象是Log類型,並且getLog方法返回的參數類型也是Log,不是LogFactory。

最關鍵的一點是:getLog方法中是通過構造器的newInstance方法創建的實例對象,每次請求getLog方法都會返回一個新的實例,它其實是一個多例模式。

7.4 ErrorContext

mybatis提供ErrorContext類記錄了錯誤資訊的上下文,方便後續處理。

那麼它是如何實現單例模式的呢?關鍵程式碼如下:

public class ErrorContext {
  ...
  private static final ThreadLocal<ErrorContext> LOCAL = new ThreadLocal<ErrorContext>();
  
  private ErrorContext() {
  }
  
  public static ErrorContext instance() {
    ErrorContext context = LOCAL.get();
    if (context == null) {
      context = new ErrorContext();
      LOCAL.set(context);
    }
    return context;
  }
  ...
}  

我們可以看到,ErrorContext跟傳統的單例模式不一樣,它改良了一下。它使用了餓漢模式,並且使用ThreadLocal,保證每個執行緒中的實例對象是單例的。這樣看來,ErrorContext類創建的對象不是唯一的,它其實也是多例模式的一種。

7.5 spring的單例

以前在spring中要定義一個bean,需要在xml文件中做如下配置:

<bean id="test" class="com.susan.Test" init-method="init" scope="singleton">

在bean標籤上有個scope屬性,我們可以通過指定該屬性控制bean實例是單例的,還是多例的。如果值為singleton,代表是單例的。當然如果該參數不指定,默認也是單例的。如果值為prototype,則代表是多例的。

在spring的AbstractBeanFactory類的doGetBean方法中,有這樣一段程式碼:

if (mbd.isSingleton()) {
    sharedInstance = getSingleton(beanName, () -> {
      return createBean(beanName, mbd, args);
  });
  bean = getObjectForBeanInstance(sharedInstance, name, beanName, mbd);
} else if (mbd.isPrototype()) {
    Object prototypeInstance = createBean(beanName, mbd, args);
    bean = getObjectForBeanInstance(prototypeInstance, name, beanName, mbd);
} else {
    ....
}

這段程式碼我為了好演示,看起來更清晰,我特地簡化過的。它的主要邏輯如下:

  1. 判斷如果scope是singleton,則調用getSingleton方法獲取實例。
  2. 如果scope是prototype,則直接創建bean實例,每次會創建一個新實例。
  3. 如果scope是其他值,則允許我們自定bean的創建過程。

其中getSingleton方法主要程式碼如下:

public Object getSingleton(String beanName, ObjectFactory<?> singletonFactory) {
		Assert.notNull(beanName, "Bean name must not be null");
		synchronized (this.singletonObjects) {
			Object singletonObject = this.singletonObjects.get(beanName);
			if (singletonObject == null) {
          singletonObject = singletonFactory.getObject();
         if (newSingleton) {
					      addSingleton(beanName, singletonObject);
				    }
			}
			return singletonObject;
		}
}

有個關鍵的singletonObjects對象,其實是一個ConcurrentHashMap集合:

private final Map<String, Object> singletonObjects = new ConcurrentHashMap<>(256);

getSingleton方法的主要邏輯如下:

  1. 根據beanName先從singletonObjects集合中獲取bean實例。
  2. 如果bean實例不為空,則直接返回該實例。
  3. 如果bean實例為空,則通過getObject方法創建bean實例,然後通過addSingleton方法,將該bean實例添加到singletonObjects集合中。
  4. 下次再通過beanName從singletonObjects集合中,就能獲取到bean實例了。

在這裡spring是通過ConcurrentHashMap集合來保證對象的唯一性。

最後留給大家幾個小問題思考一下:

  1. 多例模式 和 多對象模式有什麼區別?
  2. java框架中有些單例模式用的不規範,我要參考不?
  3. spring的單例,只是結果是單例的,但完全沒有遵循單例模式的固有寫法,它也算是單例模式嗎?

歡迎大家給我留言,說出你心中的答案。

碼字不易,如果讀了文章有些收穫的話,請幫我點贊一下,謝謝你的支援和鼓勵。

最近無意間獲得一份BAT大廠大佬寫的刷題筆記,一下子打通了我的任督二脈,越來越覺得演算法沒有想像中那麼難了。

BAT大佬寫的刷題筆記,讓我offer拿到手軟

最後說一句(求關注,別白嫖我)
如果這篇文章對您有所幫助,或者有所啟發的話,幫忙掃描下發二維碼關注一下,您的支援是我堅持寫作最大的動力。

求一鍵三連:點贊、轉發、在看。

關注公眾號:【蘇三說技術】,在公眾號中回復:面試、程式碼神器、開發手冊、時間管理有超贊的粉絲福利,另外回復:加群,可以跟很多BAT大廠的前輩交流和學習。