單例模式:屬於創建型模式,主要用來創建對象的。保證一個類僅有一個實例,並提供一個訪問它的全局訪問點。
下面就是個簡單的單例模式:
public class Singleton {
//設置靜態變量
private static Singleton singleton;
//private構造方法能夠確保能通過new來創建對象
private Singleton(){}
//獲得對象
public static Singleton getSingleton(){
//判斷對象是否爲空,如果爲空則創建對象
if(singleton==null){
singleton = new Singleton();
}
return singleton;
}
public void showMessage(){
System.out.println("我是單例");
}
}
使用的時候我們只需要這樣就可以獲得對象:
public class SingletonPatternMain {
public static void main (String[] args) {
Singleton singleton = Singleton.getSingleton();
singleton.showMessage();
}
}
那麼簡單的一個單例模式就創建成功了,下面我們分析一下這樣一個單例模式有什麼問題。
現在有A、B兩個線程,當兩個線程同時執行到 if(singleton==null) 的時候發現singleton爲null,然後都會去創建對象,這樣單例模式就被破壞。
怎麼來解決這樣的問題呢,我們首先會想到加鎖,例如這樣:
public class Singleton {
//設置靜態變量
private static Singleton singleton;
//private構造方法能夠確保能通過new來創建對象
private Singleton(){}
//加鎖獲得對象
public synchronized static Singleton getSingleton(){
//判斷對象是否爲空,如果爲空則創建對象
if(singleton==null){
singleton = new Singleton();
}
return singleton;
}
public void showMessage(){
System.out.println("我是單例");
}
我們使用synchronized來修飾我們的getSingleton()方法,這樣就能確保只有一個線程獲取到方法鎖,來獲取對象。
那麼我們再來分析一下,這樣的單例有什麼問題呢?這裏有一個效率問題,因爲是單例模式,所以我們只需要在第一次創建
對象的時候加鎖就可以了。
所以我們可以這樣來修改我們代碼:利用雙重鎖定
public class Singleton {
//設置靜態變量
private static Singleton singleton;
//private構造方法能夠確保能通過new來創建對象
private Singleton(){}
//獲得對象
public static Singleton getSingleton(){
//判斷對象是否爲空,如果爲空則創建對象
if(singleton==null){
synchronized (Singleton.class){
if(singleton==null){
singleton = new Singleton();
}
}
}
return singleton;
}
public void showMessage(){
System.out.println("我是單例");
}
}
這樣我們就可以解決上面的問題。但是,從jvm角度出發這並不是一個完美的方案,因爲jvm有個叫做指令重排的概念,在單線程下沒有問題,但是在多線程下就會產生不確定的執行效果,我們來分析一下:
singleton = new Singleton();並不是一個原子性的操作,可以抽象爲JVM指令:
memory =allocate(); //1:分配對象的內存空間
ctorInstance(memory); //2:初始化對象
singleton =memory; //3:設置singleton指向剛分配的內存地址
上面操作2依賴於操作1,但是操作3並不依賴於操作2,所以JVM是可以針對它們進行指令的優化重排序的,經過重排序後如下:
memory =allocate(); //1:分配對象的內存空間
singleton =memory; //3:singleton 指向剛分配的內存地址,此時對象還未初始化
ctorInstance(memory); //2:初始化對象
比如現在有A、B兩個線程
1、A首先進入synchronized塊,由於singleton爲null,所以它執行singleton = new Singleton();
2、這時候剛好碰到指令重排,A線程先回分配對象的內存空間,然後singleton去指向剛分配的內存地址(這時候並沒有去
初始化對象) 然後A離開了synchronized塊,緊接着B線程獲得鎖,去判斷singleton是否爲空,發現singleton不爲空,
然後返回了singleton對象,這時候去使用singleton的話就會出錯。
所以我們可以使用volatile關鍵字來設置一個內存屏障來防止指令重排:private static volatile Singleton singleton;
有時候爲了實現慢加載,並且不希望每次調用getInstance時都必須互斥執行,有一種既方便又簡單的解決方法:
public class Singleton01 {
//阻止通過new來獲取對象
private Singleton01 () { }
public static Singleton01 getSingleton(){
return getInstance.singleton01;
}
//通過靜態內部類來創建對象
private static class getInstance{
private static final Singleton01 singleton01 = new Singleton01();
}
}
這種方法是通過靜態內部類來創建單例,我們來分析一下:
1、虛擬機在首次加載Java類時,會對靜態初始化塊、靜態成員變量、靜態方法進行一次初始化;靜態內容首先被加載,相當於 全局的成員
2、當靜態方法geSingleton() 加載時,SingletonHolder.INSTANCE默認初始值是空
3、當去調用geSingleton() 方法時,靜態內部類getInstance纔去加載,從而去創建Singleton01對象(常量)
要點:
1、線程安全:因爲內部的靜態類只會被加載一次,只會有一個實例對象,所以是線程安全的
2、內部類的加載機制: java中的內部類是延時加載的,只有在第一次使用時加載;不使用就不加載;