一、單例模式的定義
單例模式屬於對象建立性模式,具備以下特點的類成為單例類。
1、該類只有一個唯一的執行個體
2、該唯一的執行個體是由自己執行個體化的
3、該類自行向整個系統提供這個執行個體
應該說單例模式是許多設計模式中最簡單的一種,它只涉及到該類本身。但是,它的思想和功能卻是強大的。在實際開發中,單例模式在什麼地方會用到呢?
比如說線程池,我們一般只需要一個線程池去管理線程,這裡就可以使用單例。又比如jdbc中的資料庫連接,因為開啟一個串連要消耗大量的資源,所以這裡也可以使用單例。
二、單例模式的優點和適用情境
優點:
1、在記憶體中只有一個對象,節省記憶體空間。
2、避免頻繁的建立銷毀對象,可以提高效能。
3、避免對共用資源的多重佔用。
4、可以全域訪問。
適用情境:由於單例模式的以上優點,所以是編程中用的比較多的一種設計模式。比較常見的適用情境:
1、需要頻繁執行個體化然後銷毀的對象。
2、建立對象時耗時過多或者耗資源過多,但又經常用到的對象。
3、有狀態的工具類對象。
4、頻繁訪問資料庫或檔案的對象。
5、以及其他我沒用過的所有要求只有一個對象的情境。
三、單例模式的各種實現方式以及注意事項
1、首先看經典的單例實現方式,也稱為為懶漢式單例
public class Singleton {private static Singleton singleton;private Singleton() {}public static Singleton getInstance(){if(singleton==null)singleton=new Singleton();return singleton;}}
在這種情況下,通過將構造器私人化的方式,來避免在類外對該類進行執行個體化(暫時不討論反射的情況),然後通過一個公有的靜態方法,執行個體化唯一的執行個體,並向整個系統提供該執行個體。
因為只有我們需要使用改類的執行個體的時候,才會去調用靜態方法getInstance(),才會去執行個體化這個唯一的執行個體,而並不是在類載入的時候就去執行個體化這個唯一的執行個體,所以稱為懶漢式單例。
實現非常的簡潔,但是確實有瑕疵的。在多線程的情況下,你可能會產生多個執行個體,而並不是你所期待的那樣:唯一的一個。也就是,上面的方式,實際上並不是安全執行緒的。
假設線上程1中,你需要使用Singleton的執行個體,你調用getInstance()方法,因為此前並沒有在任何地方調用過getInstance()方法,所以if(singleton==null) 判斷為true,那麼就要進入if代碼塊,進行執行個體化。而在這個時候,線上程2中,也需要使用Singleton的執行個體,因為線上程1中只是進入了if代碼塊,還沒來得及執行個體化,所以線上程2中,if(singleton==null)判斷依然為true,也進入了if代碼塊。問題出來了,因為多線程的並發問題,導致了得到了兩個不一樣的Singleton類的執行個體。違背了單例設計模式的初衷了。
解決單例中安全執行緒的方式有很多種,下面仔細介紹。
2、懶漢式單例模式的改寫——直接加上線程同步
public class Singleton {private static Singleton singleton;private Singleton() {}public static synchronized Singleton getInstance(){if(singleton==null){singleton=new Singleton();}return singleton;}}
在getInstance()方法上,直接加上synchronized關鍵字,將它變為同步方法,那麼安全執行緒問題非常輕鬆的解決了。可以這樣做,卻是大大降低了效率。因為線程同步時非常消耗資源的。所以不推薦使用這種方法
3、懶漢式單例模式的改寫——雙重檢查加鎖
public class Singleton {private static volatile Singleton singleton;private Singleton() {}public static Singleton getInstance(){if(singleton==null){synchronized (Singleton.class) {if(singleton==null)singleton=new Singleton();}}return singleton;}}
如果如第2種方式,直接同步處理方法,那麼每一次調用getInstance()方法的時候,都會進行同步,效率大大的降低了。使用雙重檢查加鎖,只有當singleton為null的時候,才會進入到同步代碼塊。
要注意:singleton的聲明一定要加上volatile關鍵字,看到許多人使用雙重檢查加鎖的時候 都少了這個volatile關鍵字,這樣是不正確的。關於這個volatile的用處,看以參考下面的文章:http://blog.csdn.net/lonelyroamer/article/details/8133572中的
清單 3. 將 volatile 變數用於一次性安全發布
注意:在jdk1.4及更早的版本中,許多JVM對於volatile關鍵字的實現會導致雙重檢查加鎖的失效。所以只能在jdk1.5之後使用。
4、懶漢式單例模式的改寫——餓漢式單例
public class Singleton {private static final Singleton singleton=new Singleton();private Singleton() {}public static Singleton getInstance(){return singleton;}}
直接在類載入的時候就將singleton執行個體化,這樣就能保持只有一個唯一的執行個體,也就沒有上面所說的安全執行緒的問題。
4、使用靜態私人內部類建立單例執行個體
public class Singleton {//靜態內部私人類private static class SingletonHolder {static final Singleton uniqueInstance = new Singleton();}private Singleton() {}public static Singleton getInstance() {return SingletonHolder.uniqueInstance;}}
因為靜態私人內部類SingletonHolder不會隨Singleton的載入而載入,它只會在使用到它的時候再去載入。所以,它具有懶漢式延遲執行個體化的特點,並且在類載入初始化的時候,所有變數都是自動同步的,也就是說是安全執行緒的。
這種方式,十分的巧妙,有懶漢式單例的延遲建立,又沒有它的安全執行緒問題。個人覺得這個方式在java中非常不錯。
5、單例的繼承結構---註冊式單例
看代碼:
package com.tao.pattern;import java.lang.reflect.Constructor;import java.lang.reflect.InvocationTargetException;import java.util.HashMap;public class JavaTest{public static void main(String[] args) {SingletonChild.getInstance().show();}} class Singleton {protected Singleton(){}private static final HashMap<String,Singleton> m_register=new HashMap<String, Singleton>();public static Singleton getInstance(Class<? extends Singleton> clazz){if(clazz!=null){if(!m_register.containsKey(clazz.getName())){synchronized(Singleton.class){if(!m_register.containsKey(clazz.getName())){try {Constructor<? extends Singleton> constructor=clazz.getDeclaredConstructor(new Class[]{});constructor.setAccessible(true);Singleton singleton=constructor.newInstance();if(singleton!=null)m_register.put(clazz.getName(), singleton);} catch (NoSuchMethodException | SecurityException e) {e.printStackTrace();} catch (InstantiationException e) {e.printStackTrace();} catch (IllegalAccessException e) {e.printStackTrace();} catch (IllegalArgumentException e) {e.printStackTrace();} catch (InvocationTargetException e) {e.printStackTrace();}}}}return m_register.get(clazz.getName());}else {return null;}}}class SingletonChild extends Singleton{private SingletonChild(){}public static SingletonChild getInstance(){return (SingletonChild)Singleton.getInstance(SingletonChild.class);}public void show(){System.out.println("我已經註冊了");}}
這種方式用的很少,主要用於單例模式的繼承結構,讓子類都能實現單例。一般,當你要使用這種方式的時候,還是要看看自己的設計是否有問題。
四、單例模式的使用注意
1、記憶體回收機制會不會回收單例
在Java1.2之前,記憶體回收行程有個bug,會造成當單例在沒有全域的引用時被當做垃圾清除。也就是說,如果一個單例只有本單例類引用它本身,那麼該單例就會被當做垃圾清除。
在Java1.2之後,這個bug已經被修正了,也不再需要一個全域引用來保護單例。所以對於1.2之後,單例是不會被回收的。1.2之前就需要小心了。
下面有篇文章專門討論這個問題:http://blog.csdn.net/zhengzhb/article/details/7331354
2、反射破壞單例原則
public class Test {public static void main(String[] args) throws NoSuchMethodException,SecurityException, InstantiationException, IllegalAccessException,IllegalArgumentException, InvocationTargetException {Singleton regSingleton1 = Singleton.getInstance();Singleton regSingleton2 = Singleton.getInstance();Constructor<Singleton> constructor = Singleton.class.getDeclaredConstructor(new Class[] {});constructor.setAccessible(true);Singleton regSingleton3=constructor.newInstance(null);System.out.println(regSingleton1==regSingleton2);System.out.println(regSingleton1==regSingleton3);}}
列印結果為
true
false
可以說明,反射產生了一個新的執行個體
3、不同的類載入器載入,會得到多個執行個體
4、序列化會產生多個執行個體
5、使用ThreadLocal為每個線程產生一個單例
比如在JavaWeb中,需要為每個線程產生一個session,各個session之間互不影響,這裡就可以使用ThreadLocal,來為每個線程產生session
public class Singleton {private static ThreadLocal<Singleton> thread_local=new ThreadLocal<Singleton>();private Singleton() {}public static Singleton getInstance() {if(thread_local.get()==null){thread_local.set(new Singleton());}return thread_local.get();}}
測試下:
public class Test {public static void main(String[] args) throws NoSuchMethodException,SecurityException, InstantiationException, IllegalAccessException,IllegalArgumentException, InvocationTargetException {final Singleton singleton1=Singleton.getInstance();Singleton singleton2=Singleton.getInstance();System.out.println(singleton1==singleton2);new Thread(){public void run() {Singleton singleton=Singleton.getInstance();System.out.println(singleton==singleton1);};}.start();}}
列印結果為:
true
false
說明,每個線程都會拿到一個執行個體的副本,不一樣的單例執行個體。