天天看點

設計模式之美------單例模式【一篇學會單例模式寫法】單例模式

單例模式

顧名思義,單例模式的意思就是隻有1個執行個體的對象。就像天天上的太陽和月亮隻有一個一樣。

單例模式有很多種寫法,在這裡我們一一對比:

1.懶漢模式

//懶漢模式
public class Singleton1 {
	
	private static Singleton1  instance;
	
	private Singleton1() {};
	
	
	public static Singleton1 getInstance()
	{
		if(instance == null)
		{
			instance = new Singleton1();
		}
		return instance;
	}

}

           

懶漢模式是指在需要的時候再去建立對象。我們可以看到它的成員屬性中有一個靜态的執行個體對象,然後對外隐藏了自己的構造方法,是的我們隻能通過靜态方法getInstance()來擷取它的對象。而在getInstance()中,判斷了執行個體對象是否已經被初始化過。

但這個解法的缺點就是隻能在單線程模式下工作,在多線程模式下會失效。

我們測試一下,測試代碼如下:

public static void main(String[] args) {
	
//線程1嘗試擷取一個執行個體
		new Thread(new Runnable() {
			
			@Override
			public void run() {
				Singleton1 s1 = Singleton1.getInstance();
				System.out.println(s1.hashCode());
				
			}
		}).start();
		
		//線程2嘗試擷取一個執行個體
		new Thread(new Runnable() {
			
			@Override
			public void run() {
				Singleton1 s2 = Singleton1.getInstance();
				System.out.println(s2.hashCode());
				
			}
		}).start();
		
	}
           

而結果是不穩定的:

設計模式之美------單例模式【一篇學會單例模式寫法】單例模式
設計模式之美------單例模式【一篇學會單例模式寫法】單例模式

會出現這樣兩種情況,是以在多線程情況下不推薦使用。

2.餓漢模式

public class Singleton2 
{
	private static Singleton2 instance = new Singleton2();
	private Singleton2() {};
	
	public static Singleton2 getInstance()
	{
		return instance;
	}

}

           

餓漢模式解決了線程安全的問題,但是我們可以看到,因為instance對象是靜态的,在類初始化之後,這個對象就已經存在了。可能我們并不需要它,但它仍然會占用記憶體。

3.加鎖模式

public class Singleton3 {
	
	private Singleton3() {};
	static Lock lock = new ReentrantLock();
	private static  Singleton3 instance = null;
	
	private static Object objSingleton3 = new Object();
	
	public static Singleton3 getInstance()
	{
		
	synchronized (objSingleton3) {
		

			if(instance == null)
			{
				instance = new Singleton3();
			
			
			}
		}
	
	return instance;
	}

}
           

這樣相比懶漢模式解決了線程安全問題,也不會在不使用的情況下生成對象,但加鎖的過程也是較為耗時的。

public class Singleton4 {
	
	private static Singleton4 instance;
	
	private Singleton4() {};
	
	
	 synchronized public static Singleton4 getInstance()
	{
		 if(instance == null)
		 {
			 instance = new Singleton4();
		 }
		 return instance;
	}

}
           

這種方式和上面的是一樣的,就是寫法不一樣,都是效率比較低。

然後還有一種比較類似的寫法,但卻是線程不安全的,

public class Singleton4 {
	
	private static Singleton4 instance;
	
	private Singleton4() {};
	
	
	  public static Singleton4 getInstance()
	{
		 if(instance == null)//-------①
		 {
			 synchronized (Singleton4.class) {
				
				 instance = new Singleton4();
			}
			 
		 }
		 return instance;
	}

}
           

這裡我們隻鎖了代碼塊,但在多線程情況下,線程A可能在①處讓出cpu,然後線程B仍然可以獲得鎖,然後建立對象。當線程A重新拿到CPU的時候,還會繼續建立新的對象,這樣就不是線程安全的 了。

4.雙重檢查模式

public class Singleton4 {
	
	private static volatile Singleton4 instance;-------③
	public  volatile static int a=0;
	
	private Singleton4() {};
	
	
	  public static Singleton4 getInstance()
	{
		 if(instance == null)------①
		 {
			 synchronized (Singleton4.class) {
				
				 if(instance == null)-------②
				 {
					
				 instance = new Singleton4();
				 }
			}
			 
		 }
		 return instance;
	}

}
           

在我們進行初始化的時候,檢查了兩次instance對象是否為空。代碼①處的檢查是因為要判斷instance是否已經存在,避免不必要的上鎖。代碼②處的檢查是因為為了解決并發情況下是否在加鎖後再次确認。這種方式相較餓漢式提高了效率,在用到的時候進行初始化,節省了空間,相對于懶漢式能夠保證線程安全。而代碼③處的volatile關鍵字保證了instance對象的可見性。避免因為jvm底層指令執行順序的原因導緻出現意料之外的錯誤。

5.靜态内部類

public class Singleton5 {
	
	private Singleton5() {};
	
	public static Singleton5 getInstance()
	{
		return Innerclass.instance;
	}
	
	
	 private static class Innerclass{
		
		
		private static final Singleton5 instance = new Singleton5();
	}

}
           

這個模式結合了餓漢式和懶漢式的優點。它隻會在我們第一次使用這個Innerclass的時候,會調用它的構造函數去建立這個屬性,如果我們不使用Innerclass,就不會去初始化instance 。

6.枚舉法

public enum Singleton6 {
	Singleton6;

}
           

使用的時候直接Singleton6.Singleton6就可以拿到對象。而且枚舉類有且隻有私有構造器。

而且對序列化也有很好的支援。

總結一下:

單例模式三大要素:

1.私有構造函數:確定不會被外部類通路

2.自己持有,或者通過内部類持有自己類型的執行個體對象

3.對外暴露一個擷取對象的靜态方法

對比一下幾種方式:

  1. 懶漢模式:隻能在單線程中使用
  2. 餓漢模式:建立執行個體的時機不确定,可能會浪費記憶體
  3. 加鎖模式:線程安全,但會在一定程度上降低代碼執行的小綠
  4. 雙重檢驗模式:線程安全,隻在第一次建立執行個體的時候會進行加鎖,效率也比較高
  5. 靜态内部類:無需加鎖,簡潔直覺,僅在需要的時候建立執行個體。
  6. 枚舉法:簡潔友善,自動支援了序列化,但可讀性比較低。

綜上,在一般開發的時候會比較常用4/5,但也不是唯一的,在不同的場景中可以使用不同的方法才使最好的。

繼續閱讀