什麼是單例
確保某個類在系統運行中只能產生一個唯一的實例。
來一個不太規矩的例子
新建Singleton類,並將其構造方法設爲private,然後想辦法獲取這個類的實例。
public class Singleton {
private static Singleton uniqueInstance;
//私有的構造器,只有自己能訪問,換言之只有自己能創建自己的實例
private Singleton(){
System.out.println("創建了一個實例");
}
public static Singleton getInstance(){
//如果當前實例爲空,則創建一個,否則返回已經創建好的實例
if (uniqueInstance == null){
uniqueInstance = new Singleton();
}
System.out.println("得到了一個實例");
return uniqueInstance;
}
}
寫一個測試類。
先試着傳統的new一下,你會發現會報錯的。因爲Singleton的構造器是私有的,除了在他自己內部,誰也調用不了。
public class Test {
public static void main(String[] args){
//報'Singleton()' has private access in 'Singleton'錯誤
Singleton singleton = new Singleton();
}
}
所以我們只能拿過來已有的實例,而不是去創建。
public class Test {
public static void main(String[] args){
/*
* 這裏寫了五個,測試一下是不是隻會得到一個實例
* */
Singleton singleton = Singleton.getInstance();
Singleton singleton1 = Singleton.getInstance();
Singleton singleton2 = Singleton.getInstance();
Singleton singleton3 = Singleton.getInstance();
Singleton singleton4 = Singleton.getInstance();
}
}
輸出結果
創建了一個實例
得到了一個實例
得到了一個實例
得到了一個實例
得到了一個實例
得到了一個實例
通過結果發現,確實只是在第一次使用時創建了出一個實例,以後只是拿過來用了。
但是,仔細想一個問題,這是單線程的情況。如果有兩個線程同時執行到if判斷處,會怎樣?
/*
* 加入兩個線程同時到了判斷處,發現還沒有當前類的實例,所有都會執行到new
* 這樣就會創建出兩個實例,這就違背了我們的初衷
* */
if (uniqueInstance == null){
uniqueInstance = new Singleton();
}
System.out.println("得到了一個實例");
return uniqueInstance;
用多線程測試一下(需多次測試,會發現會有不同的結果)。
public class MyThread implements Runnable {
@Override
public void run() {
Singleton singleton = Singleton.getInstance();
}
}
public class Test {
public static void main(String[] args){
Thread thread = new Thread(new MyThread());
thread.start();
Thread thread1 = new Thread(new MyThread());
thread1.start();
Thread thread2 = new Thread(new MyThread());
thread2.start();
Thread thread3 = new Thread(new MyThread());
thread3.start();
Thread thread4 = new Thread(new MyThread());
thread4.start();
}
}
測試結果:
創建了一個實例
得到了一個實例
創建了一個實例
得到了一個實例
得到了一個實例
得到了一個實例
得到了一個實例
這裏發現,確實違背了我們的初衷,在多個線程中,我們寫的單例已經出了問題,現在系統中已經存在了兩個實例。這種情況,我們也稱之爲非線程安全。所以剛剛這個例子稱不上真正意義上的單例模式。
有點笨拙的單例模式
剛剛問題的根源是由於多個線程同時操作了一個資源,那麼我們在訪問時每次限定在一個線程就好了。
Java中有一個synchronized關鍵字,可用來給對象和方法或者代碼塊加鎖,當它鎖定一個方法或者一個代碼塊的時候,同一時刻最多隻有一個線程執行這段代碼。當兩個併發線程訪問同一個對象中的這個加鎖同步代碼塊時,一個時間內只能有一個線程得到執行。另一個線程必須等待當前線程執行完這個代碼塊以後才能執行該代碼塊。
所以我們給獲取實例的方法,加上synchronized關鍵字。
public class Singleton {
private static Singleton uniqueInstance;
//私有的構造器,只有自己能訪問,換言之只有自己能創建自己的實例
private Singleton(){
System.out.println("創建了一個實例");
}
//有了synchronized關鍵字時候,當多個線程同時訪問時,一次只能接待一個線程,剩下的線程排隊等候。
public static synchronized Singleton getInstance(){
//如果當前實例爲空,則創建一個,否則返回已經創建好的實例
if (uniqueInstance == null){
uniqueInstance = new Singleton();
}
System.out.println("得到了一個實例");
return uniqueInstance;
}
}
測試結果
創建了一個實例
得到了一個實例
得到了一個實例
得到了一個實例
得到了一個實例
得到了一個實例
現在的結果是符合我們的定義的。
但是又有一個新問題出現,只有在第一次判斷實例非空的時候才需要synchronized控制,避免第一次同時創建多個實例,第一次實例創建之後就不需要synchronized了,之後的每一獲取實例,對於多線程來說將使得程序的性能大大降低。
懶漢與餓漢
這裏引入一個懶漢與餓漢的概念。
- 懶漢:當使用到該實例時,纔會去創建該類的實例。
- 餓漢:程序初始化時就會創建該實例。
所以,剛剛我們的代碼屬於“懶漢”模式,在運行時纔會創建實例,所以會出現非線程安全的問題。那麼如果設計成“餓漢”模式呢?讓程序一開始就創建好該實例,提前就準備好,免去了判斷的麻煩,以後用到的時候直接獲取就可以了。
單例-餓漢
修改我們的代碼
public class Singleton {
private static Singleton uniqueInstance = new Singleton();
//私有的構造器,只有自己能訪問,換言之只有自己能創建自己的實例
private Singleton(){
System.out.println("創建了一個實例");
}
public static synchronized Singleton getInstance(){
System.out.println("得到了一個實例");
//因爲已經存在了實例,所以直接獲取就可以了
return uniqueInstance;
}
}
測試運行結果
創建了一個實例
得到了一個實例
得到了一個實例
得到了一個實例
得到了一個實例
得到了一個實例
這樣解決了非線程安全和性能慢的問題。但這種餓漢模式,也有一個缺點,因爲在初始化時就創建了一個類的實例,所以會耗費一定的系統資源。