設計模式——單例模式

單例模式:屬於創建型模式,主要用來創建對象的。保證一個類僅有一個實例,並提供一個訪問它的全局訪問點。

下面就是個簡單的單例模式:

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中的內部類是延時加載的,只有在第一次使用時加載;不使用就不加載;

 

 

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