加載,表示啓動程序的文件或信息的載入。在Java中類的字節碼文件加載進內存,就是以io流的形式存入內存。
User user=new User("男",18);
該語句做了幾件事:
- 因爲new用到了User.class,所以找到User.class文件並加載到內存中
- 執行該類的static代碼塊,如果有的話,給User.class類進行初始化
- 在堆內存中開闢空間,分配內存地址。
- 在堆內存中建立對象的特有屬性,並進行默認初始化。
- 對屬性進行顯式初始化
- 對對象進行構造代碼塊初始化
- 對對象進行對應的構造函數初始化
- 將內存地址賦給棧內存中的p變量
特別注意,步驟7和步驟8,先後發生順序是隨機的
懶加載
餓漢式單例
public class Singleton {
private static Singleton instance = new Singleton();
private Singleton(){
…
}
public static Singleton getInstance(){
return instance;
}
}
這樣的代碼缺點是:第一次加載類的時候會連帶着創建Singleton實例,這樣的結果與我們所期望的不同,因爲創建實例的時候可能並不是我們需要這個實例的時候。同時如果這個Singleton實例的創建非常消耗系統資源,而應用始終都沒有使用Singleton實例,那麼創建Singleton消耗的系統資源就被白白浪費了。
爲了避免這種情況,我們通常使用惰性加載的機制,也就是在使用的時候纔去創建。以上代碼的惰性加載代碼如下:
懶漢式單例
public class Singleton{
private static Singleton instance = null;
private Singleton(){
…
}
public static Singleton getInstance(){
if (instance == null)
instance = new Singleton();
return instance;
}
}
線程安全問題
這是如果兩個線程A和B同時執行了該方法,然後以如下方式執行:
- A進入if判斷,此時instance爲null,因此進入if內
- B進入if判斷,此時A還沒有創建instance,因此instance也爲null,因此B也進入if內
- A創建了一個instance並返回
- B也創建了一個instance並返回
此時問題出現了,我們的單例被創建了兩次,而這並不是我們所期望的。
各種解決方案及其存在的問題
使用Class鎖機制
以上問題最直觀的解決辦法就是給getInstance方法加上一個synchronize前綴,這樣每次只允許一個現成調用getInstance方法:
public static synchronized Singleton getInstance(){
if (instance == null)
instance = new Singleton();
return instance;
}
這種解決辦法的確可以防止錯誤的出現,但是它卻很影響性能:每次調用getInstance方法的時候都必須獲得Singleton的鎖,而實際上,當單例實例被創建以後,其後的請求沒有必要再使用互斥機制了
雙重檢查鎖定 (double-checked locking)
曾經有人爲了解決以上問題,提出了double-checked locking的解決方案
public static Singleton getInstance(){
if (instance == null)
synchronized(instance){
if(instance == null)
instance = new Singleton();
}
return instance;
}
讓我們來看一下這個代碼是如何工作的:
首先當一個線程發出請求後,會先檢查instance是否爲null,如果不是則直接返回其內容,這樣避免了進入synchronized塊所需要花費的資源。
其次,即使第2節提到的情況發生了,兩個線程同時進入了第一個if判斷,那麼他們也必須按照順序執行synchronized塊中的代碼,第一個進入代碼塊的線程會創建一個新的Singleton實例,而後續的線程則因爲無法通過if判斷,而不會創建多餘的實例。
上述描述似乎已經解決了我們面臨的所有問題,但實際上,從JVM的角度講,這些代碼仍然可能發生錯誤。
對於JVM而言,它執行的是一個個Java指令。在Java指令中創建對象和賦值操作是分開進行的,也就是說instance = new Singleton();語句是分兩步執行的。但是JVM並不保證這兩個操作的先後順序,也就是說有可能JVM會爲新的Singleton實例分配空間,然後直接賦值給instance成員,然後再去初始化這個Singleton實例。(即先賦值指向了內存地址,再初始化)這樣就使出錯成爲了可能,我們仍然以A、B兩個線程爲例:
- A、B線程同時進入了第一個if判斷
- A首先進入synchronized塊,由於instance爲null,所以它執行instance = new Singleton();
- 由於JVM內部的優化機制,JVM先畫出了一些分配給Singleton實例的空白內存,並賦值給instance成員(注意此時JVM沒有開始初始化這個實例),然後A離開了synchronized塊。
- B進入synchronized塊,由於instance此時不是null,因此它馬上離開了synchronized塊並將結果返回給調用該方法的程序。
- 此時B線程打算使用Singleton實例,卻發現它沒有被初始化,於是錯誤發生了。
使用volatile
對於上述的問題,我們可以通過把instance聲明爲volatile型來解決,但是必須在JDK5版本以上使用。
private volatile static Singleton instance;
public static Singleton getSingleton() {
if (instance == null) {
synchronized (Singleton.class) {
if (instance == null) {
instance = new Singleton();
}
}
}
return instance ;
}
通過內部類實現多線程環境中的單例模式
爲了實現慢加載,並且不希望每次調用getInstance時都必須互斥執行,最好並且最方便的解決辦法如下:
public class Singleton{
private Singleton(){
…
}
private static class SingletonContainer{
private static Singleton instance = new Singleton();
}
public static Singleton getInstance(){
return SingletonContainer.instance;
}
}
JVM內部的機制能夠保證當一個類被加載的時候,這個類的加載過程是線程互斥的。這樣當我們第一次調用getInstance的時候,JVM能夠幫我們保證instance只被創建一次,並且會保證把賦值給instance的內存初始化完畢,這樣我們就不用擔心雙重檢查鎖定
中的問題。此外該方法也只會在第一次調用的時候使用互斥機制,這樣就解決了使用Class鎖機制
中的低效問題。
最後instance是在第一次加載SingletonContainer類時被創建的,而SingletonContainer類則在調用getInstance方法的時候纔會被加載,因此也實現了惰性加載。
如果文章的內容對你有幫助,歡迎關注公衆號:優享JAVA(ID:YouXiangJAVA),那裏有更多的技術乾貨,並精心準備了一份程序員書單。期待你的到來!