1. 程式人生 > >單例設計模式的幾種寫法(java版本、超詳細)

單例設計模式的幾種寫法(java版本、超詳細)

簡介

單例模式是一種常用的軟體設計模式,其定義是單例物件的類只能允許一個例項存在。這篇部落格很精彩哦,請一定要耐心看完哦

在Java設計模式中,單例模式相對來說算是比較簡單的一種構建模式。適用的場景在於:對於定義的一個類,在整個應用程式執行期間只有唯一的一個例項物件。如Android中常見的Application物件。

基本思路

單例模式要求類能夠有返回物件一個引用(永遠是同一個)和一個獲得該例項的方法(必須是靜態方法,通常使用getInstance這個名稱)。

單例的實現主要是通過以下兩個步驟:

  1. 將該類的構造方法定義為私有方法,這樣其他處的程式碼就無法通過呼叫該類的構造方法來例項化該類的物件,只有通過該類提供的靜態方法來得到該類的唯一例項;
  2. 在該類內提供一個靜態方法,當我們呼叫這個方法時,如果類持有的引用不為空就返回這個引用,如果類保持的引用為空就建立該類的例項並將例項的引用賦予該類保持的引用。

注意事項

單例模式在多執行緒的應用場合下必須小心使用。如果當唯一例項尚未建立時,有兩個執行緒同時呼叫建立方法,那麼它們同時沒有檢測到唯一例項的存在,從而同時各自建立了一個例項,這樣就有兩個例項被構造出來,從而違反了單例模式中例項唯一的原則。 解決這個問題的辦法是為指示類是否已經例項化的變數提供一個互斥鎖(雖然這樣會降低效率)。

這邊主要介紹常用的幾種:餓漢式、懶漢式、靜態內部類

1,餓漢式

Singleton.java

package mode.singleton;

public class Singleton {

	private Singleton() {};//私有話構造方法,這是單利設計模式的關鍵所在
	
	private final static Singleton INSTANCE = new Singleton();
	/**
	 * 餓漢式
	 * @return Singleton
	 */
	public static Singleton getSingleton1() {
		return INSTANCE;
	}
	
}

餓漢式比較簡單,所以就不做執行的顯示了。

優點:這種寫法比較簡單,就是在類裝載的時候就完成例項化。避免了執行緒同步問題。

缺點:在類裝載的時候就完成例項化,沒有達到Lazy Loading的效果。如果從始至終從未使用過這個例項,則會造成記憶體的浪費。

2,懶漢式(執行緒不安全)

Singleton.java

package mode.singleton;

public class Singleton {

	private Singleton() {};//私有話構造方法,這是單利設計模式的關鍵所在
	private static Singleton instance;
	
	/**
	 * 懶漢式(又稱延遲載入)
	 * @return
	 */
	public static Singleton getSingleton() {
		if(null == instance) {
			instance = new Singleton();
		}
		return instance;
	}
	
}

上述程式碼是執行緒不安全的,為什麼說執行緒不安全呢?就是當多個執行緒同時呼叫上述程式碼可能返回多個不同的例項,這違背了單利設計模式的思想。為了顯示測試的效果,我模擬執行緒不安全的場景,程式碼如下:

Singleton.java

package mode.singleton;

public class Singleton {

	private Singleton() {};//私有話構造方法,這是單利設計模式的關鍵所在
	private static Singleton instance;
	
	public static Singleton getSingleton() {
		if(null == instance) 
			try {
				Thread.currentThread().sleep(1000);//模擬不安全的場景
			} catch (InterruptedException e) {
				// TODO Auto-generated catch block
				e.printStackTrace();
			}
			instance = new Singleton();
		}
		return instance;
	}
	
}

SingleClient.java

package mode.singleton;

public class SingleClient {

	public static void main(String[] args) {
		Thread t1 = new Thread(new Runnable() {
			
			@Override
			public void run() {
				// TODO Auto-generated method stub
				Singleton s1 = Singleton.getSingleton();
				System.out.println(s1);
			}
		});
		
		Thread t2 = new Thread(new Runnable() {
			
			@Override
			public void run() {
				// TODO Auto-generated method stub
				Singleton s2 = Singleton.getSingleton();
				System.out.println(s2);
			}
		});
		t1.start();
		t2.start();
		
	}
}

 執行結果:

很明顯的可以看出上述返回的物件不是同一個物件,所以這是執行緒不安全的,不能使用

3,懶漢式(執行緒同步,同步整個方法,效能差不推薦使用)

Singleton.java

package mode.singleton;

public class Singleton {

	private Singleton() {};//私有話構造方法,這是單利設計模式的關鍵所在
	private static Singleton instance;
	/**
	 * 懶漢式,同步方法,效率低
	 * @return
	 */
	public static synchronized Singleton getSingleton() {
		if(null == instance) {
			instance = new Singleton();
		}
		return instance;
	}
	
}

雖然解決了執行緒安全問題,但是帶來了效能問題,為什麼說效率低呢?因為該方法是同步了整個方法,如果多個執行緒同時呼叫該方法時,需要一個個排隊(相當於序列),雖然這裡面的程式碼不多,但是執行緒多的話,就會累計起來,對效能造成影響。等下將該方法和下面的double check的效能做對比。

4,雙重校驗

Singleton.java

package mode.singleton;

public class Singleton {

	private Singleton() {};//私有話構造方法,這是單利設計模式的關鍵所在
	private static Singleton instance;
	
	/**
	 * double check推薦使用
	 * @return
	 */
	public static Singleton getSingleton() {
		if(null == instance) {
			synchronized (Singleton.class) {
				if(null == instance) {
					instance = new Singleton();
				}
			}
		}
		return instance;
	}
	
}

首先這個時執行緒安全的,我們看下執行的結果,客戶端程式碼同剛才不安全的懶漢式程式碼一樣:

這裡返回的兩個物件時一樣的,說明這個是執行緒安全的,並且效能也非常好。下面將該方法通上面的同步整個方法效能對比

Singleton.java

package mode.singleton;

import java.sql.Time;

public class Singleton {

	private Singleton() {};//私有話構造方法,這是單利設計模式的關鍵所在
	private static Singleton instance;

	/**
	 * 懶漢式,同步方法,效率低
	 * @return
	 */
	public static synchronized Singleton getSingleton2() {
		long start = System.currentTimeMillis();
		System.out.println("start = "+start);
		try {
			Thread.currentThread().sleep(500);//模擬耗時,方便直觀顯示
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		if(null == instance) {
			instance = new Singleton();
		}
		long end = System.currentTimeMillis();
		System.out.println("end = "+end);
		return instance;
	}
	
	/**
	 * double check推薦使用
	 * @return
	 */
	public static Singleton getSingleton() {
		long start = System.currentTimeMillis();
		System.out.println("start = "+start);
		try {
			Thread.currentThread().sleep(500);//模擬耗時,方便直觀顯示
		} catch (InterruptedException e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		if(null == instance) {
			synchronized (Singleton.class) {
				if(null == instance) {
					instance = new Singleton();
				}
			}
		}
		long end = System.currentTimeMillis();
		System.out.println("end = "+end);
		return instance;
	}
	
}

SingletonClient.java

package mode.singleton;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class SingleClient {

	public static void main(String[] args) {
		ExecutorService pool = Executors.newFixedThreadPool(5);
		for(int i=0;i<5;i++) {
			Thread thread = new Thread(new Runnable() {
				
				@Override
				public void run() {
					// TODO Auto-generated method stub
					Singleton singleton = Singleton.getSingleton2();//double check
					//Singleton singleton2 = Singleton.getSingleton();//同步方法
				}
			});
			pool.execute(thread);
		}
		pool.shutdown();
	}
}

運行同步方法結果:

執行時間end(max)-start(min) = 2504從列印的結果來看,基本上是序列的,所以比較耗時

執行double check結果:

執行時間end(max)-start(min) = 502,從列印的結果來看,是並行的,既解決了執行緒安全、又保證了效能

5,靜態內部類

Singleton.java

package mode.singleton;

import java.sql.Time;

public class Singleton {

	private Singleton() {};//私有話構造方法,這是單利設計模式的關鍵所在
	private static Singleton instance;
	
	/**
	 * 靜態內部類
	 * @return
	 */
	public static Singleton getSingleton() {
		return SingletonInstance.INSTANCE;
	}
	private static class SingletonInstance {
        private static final Singleton INSTANCE = new Singleton();
    }
			
}

這種方式跟餓漢式方式採用的機制類似,但又有不同。兩者都是採用了類裝載的機制來保證初始化例項時只有一個執行緒不同的地方在餓漢式方式是隻要Singleton類被裝載就會例項化,沒有Lazy-Loading的作用,而靜態內部類方式在Singleton類被裝載時並不會立即例項化,而是在需要例項化時,呼叫getInstance方法,才會裝載SingletonInstance類,從而完成Singleton的例項化。

  類的靜態屬性只會在第一次載入類的時候初始化,所以在這裡,JVM幫助我們保證了執行緒的安全性,在類進行初始化時,別的執行緒是無法進入的。

優點:避免了執行緒不安全,延遲載入,效率高。

我們來測試下上面描述的情況:內部類不會隨著外部類的載入而載入,只有當用到時再載入(延時載入)

SingletonClient.java

package mode.singleton;

import java.sql.Time;

public class Singleton {

	private Singleton() {};//私有話構造方法,這是單利設計模式的關鍵所在
	private static Singleton instance;
	
	
	static {
    	System.out.println("load Single");
    }
	
	public static void testLoad() {}//測試載入
	
	/**
	 * 靜態內部類
	 * @return
	 */
	public static Singleton getSingleton() {
		return SingletonInstance.INSTANCE;
	}
	
	private static class SingletonInstance {
        private static final Singleton INSTANCE = new Singleton();
        static {
        	System.out.println("load SingletonInstance");
        }
    }
	
	
}

SingletonClient.java

package mode.singleton;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class SingleClient {

	public static void main(String[] args) {
		Singleton.testLoad();
	}
}

執行結果:

從執行結果看出內部類並沒有隨著外部類的載入而載入,接下來看看呼叫getSingleton()方法之後的執行結果

SingleClient.java

package mode.singleton;

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class SingleClient {

	public static void main(String[] args) {
		Singleton singleton = Singleton.getSingleton();
		System.out.println(singleton);
	}
}

執行結果:

從結果可以看出,只有當呼叫getSingleton()方法之後才會載入靜態內部類。

好了,單利設計模式就講到這裡了,基本上經常使用的就這幾個,當然還有其他的方法,比如列舉、同步程式碼塊等,這裡就不講了。如有錯誤,歡迎指正。